diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl index 67f43f7fe6..9eea8c1be1 100644 --- a/locales/en-US/browser/browser/zen-workspaces.ftl +++ b/locales/en-US/browser/browser/zen-workspaces.ftl @@ -66,6 +66,10 @@ zen-panel-ui-gradient-click-to-add = Click to add a color zen-workspace-creation-name = .placeholder = Space Name +zen-move-tab-to-workspace-button = + .label = Move To... + .tooltiptext = Move all tabs in this window to a Space + zen-workspaces-panel-context-reorder = .label = Reorder Spaces diff --git a/prefs/zen/session-store.yaml b/prefs/zen/session-store.yaml new file mode 100644 index 0000000000..dc2e97d4c4 --- /dev/null +++ b/prefs/zen/session-store.yaml @@ -0,0 +1,9 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +- name: zen.session-store.backup-file + value: true + +- name: zen.session-store.log + value: false diff --git a/prefs/zen/window-sync.yaml b/prefs/zen/window-sync.yaml new file mode 100644 index 0000000000..f3e30f29d3 --- /dev/null +++ b/prefs/zen/window-sync.yaml @@ -0,0 +1,9 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +- name: zen.window-sync.enabled + value: true + +- name: zen.window-sync.log + value: false diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 47f083c49c..fb96936332 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -45,7 +45,6 @@ # Scripts used all over the browser - diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index d94f76ae0c..b4ecd5e149 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -59,3 +59,8 @@ + + +# Popup to move tabs to a synced workspace. +# This would be automatically populated with the list of available synced workspaces. + diff --git a/src/browser/base/content/zen-preloaded.inc.xhtml b/src/browser/base/content/zen-preloaded.inc.xhtml index ab53540920..76f32fd5f5 100644 --- a/src/browser/base/content/zen-preloaded.inc.xhtml +++ b/src/browser/base/content/zen-preloaded.inc.xhtml @@ -6,7 +6,6 @@ # the window is fully loaded. # Make sure they are loaded before the global-scripts.inc file. - diff --git a/src/browser/components/places/content/editBookmark-js.patch b/src/browser/components/places/content/editBookmark-js.patch index 100ee182dd..92392184f5 100644 --- a/src/browser/components/places/content/editBookmark-js.patch +++ b/src/browser/components/places/content/editBookmark-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/places/content/editBookmark.js b/browser/components/places/content/editBookmark.js -index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4016594b9 100644 +index f562f19741d882d92365da531b55e2810a0e79ea..a68ce8191314845c589f3a9f14b56028e0532628 100644 --- a/browser/components/places/content/editBookmark.js +++ b/browser/components/places/content/editBookmark.js @@ -387,6 +387,10 @@ var gEditItemOverlay = { @@ -31,34 +31,11 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 } break; } -@@ -1280,6 +1288,148 @@ var gEditItemOverlay = { +@@ -1280,6 +1288,128 @@ var gEditItemOverlay = { get bookmarkState() { return this._bookmarkState; }, + -+ async _initWorkspaceSelector() { -+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { -+ return; -+ } -+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces(); -+ -+ const selectElement = this._workspaceSelect; -+ -+ // Clear any existing options -+ while (selectElement.firstChild) { -+ selectElement.removeChild(selectElement.firstChild); -+ } -+ -+ // For each workspace, create an option element -+ for (let workspace of this._workspaces) { -+ const option = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); -+ option.textContent = workspace.name; -+ option.value = workspace.uuid; -+ selectElement.appendChild(option); -+ } -+ -+ selectElement.disabled = this.readOnly; -+ }, + async onWorkspaceSelectionChange(event) { + if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { + return; @@ -129,7 +106,10 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 + if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { + return; + } -+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces(); ++ const { ZenSessionStore } = ChromeUtils.importESModule( ++ "resource:///modules/zen/ZenSessionManager.sys.mjs" ++ ); ++ this._workspaces = ZenSessionStore.getClonedSpaces(); + const workspaceList = this._workspaceList; + if(aInfo.node?.bookmarkGuid) { + this._selectedWorkspaces = await ZenWorkspaceBookmarksStorage.getBookmarkWorkspaces(aInfo.node.bookmarkGuid); @@ -180,7 +160,7 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 }; ChromeUtils.defineLazyGetter(gEditItemOverlay, "_folderTree", () => { -@@ -1318,6 +1468,9 @@ for (let elt of [ +@@ -1318,6 +1448,9 @@ for (let elt of [ "locationField", "keywordField", "tagsField", diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch new file mode 100644 index 0000000000..895c4313fa --- /dev/null +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs +index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644 +--- a/browser/components/sessionstore/SessionFile.sys.mjs ++++ b/browser/components/sessionstore/SessionFile.sys.mjs +@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + RunState: "resource:///modules/sessionstore/RunState.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; +@@ -380,7 +381,7 @@ var SessionFileInternal = { + this._readOrigin = result.origin; + + result.noFilesFound = noFilesFound; +- ++ await lazy.ZenSessionStore.readFile(); + return result; + }, + diff --git a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch new file mode 100644 index 0000000000..e6956ea61b --- /dev/null +++ b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch @@ -0,0 +1,20 @@ +diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs +index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644 +--- a/browser/components/sessionstore/SessionSaver.sys.mjs ++++ b/browser/components/sessionstore/SessionSaver.sys.mjs +@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + /* +@@ -305,6 +306,7 @@ var SessionSaverInternal = { + this._maybeClearCookiesAndStorage(state); + + Glean.sessionRestore.collectData.stopAndAccumulate(timerId); ++ lazy.ZenSessionStore.saveState(state); + return this._writeState(state); + }, + diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch new file mode 100644 index 0000000000..b106193cf3 --- /dev/null +++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs +index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644 +--- a/browser/components/sessionstore/SessionStartup.sys.mjs ++++ b/browser/components/sessionstore/SessionStartup.sys.mjs +@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + StartupPerformance: + "resource:///modules/sessionstore/StartupPerformance.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const STATE_RUNNING_STR = "running"; +@@ -179,6 +180,8 @@ export var SessionStartup = { + this._initialState = parsed; + } + ++ lazy.ZenSessionStore.onFileRead(this._initialState); ++ + if (this._initialState == null) { + // No valid session found. + this._sessionType = this.NO_SESSION; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 7efaefb173..c8cbd23a32 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb22176c60c4e 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e77416a2ab48 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1911,6 +1913,8 @@ var SessionStoreInternal = { +@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,19 +28,68 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2384,11 +2388,9 @@ var SessionStoreInternal = { - tabbrowser.selectedTab.label; +@@ -2020,6 +2025,10 @@ var SessionStoreInternal = { + this._windows[aWindow.__SSi].isTaskbarTab = true; + } + ++ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) { ++ this._windows[aWindow.__SSi].isZenUnsynced = true; ++ } ++ + let tabbrowser = aWindow.gBrowser; + + // add tab change listeners to all already existing tabs +@@ -2151,7 +2160,6 @@ var SessionStoreInternal = { + if (closedWindowState) { + let newWindowState; + if ( +- AppConstants.platform == "macosx" || + !lazy.SessionStartup.willRestore() + ) { + // We want to split the window up into pinned tabs and unpinned tabs. +@@ -2215,6 +2223,15 @@ var SessionStoreInternal = { + }); + this._shouldRestoreLastSession = false; + } ++ else if (!aInitialState && isRegularWindow) { ++ let windowPromises = []; ++ for (let window of this._browserWindows) { ++ windowPromises.push(lazy.TabStateFlusher.flushWindow(window)); ++ } ++ Promise.all(windowPromises).finally(() => { ++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); ++ }); ++ } + + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) +@@ -2465,7 +2482,7 @@ var SessionStoreInternal = { + // 2) Flush the window. + // 3) When the flush is complete, revisit our decision to store the window + // in _closedWindows, and add/remove as necessary. +- if (!winData.isPrivate && !winData.isTaskbarTab) { ++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) { + this.maybeSaveClosedWindow(winData, isLastWindow); } -- if (AppConstants.platform != "macosx") { - // Until we decide otherwise elsewhere, this window is part of a series - // of closing windows to quit. - winData._shouldRestore = true; -- } +@@ -2486,7 +2503,7 @@ var SessionStoreInternal = { - // Store the window's close date to figure out when each individual tab - // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3375,7 @@ var SessionStoreInternal = { + // Save non-private windows if they have at + // least one saveable tab or are the last window. +- if (!winData.isPrivate && !winData.isTaskbarTab) { ++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) { + this.maybeSaveClosedWindow(winData, isLastWindow); + + if (!isLastWindow && winData.closedId > -1) { +@@ -2582,6 +2599,7 @@ var SessionStoreInternal = { + let alreadyStored = winIndex != -1; + // If sidebar command is truthy, i.e. sidebar is open, store sidebar settings + let shouldStore = hasSaveableTabs || isLastWindow; ++ lazy.ZenSessionStore.maybeSaveClosedWindow(winData, isLastWindow); + + if (shouldStore && !alreadyStored) { + let index = this._closedWindows.findIndex(win => { +@@ -3373,7 +3391,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -41,12 +98,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 return; } -@@ -4089,6 +4091,12 @@ var SessionStoreInternal = { +@@ -4089,6 +4107,12 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; + tabState.zenEssential = false; -+ tabState.zenPinnedId = null; ++ tabState.zenSyncId = null; + tabState.zenIsGlance = false; + tabState.zenGlanceId = null; + tabState.zenHasStaticLabel = false; @@ -54,7 +111,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4533,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4549,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -62,7 +119,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5383,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5399,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -71,16 +128,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 removableTabs.push(tab); } } -@@ -5434,7 +5443,7 @@ var SessionStoreInternal = { - } +@@ -5483,7 +5508,7 @@ var SessionStoreInternal = { - let workspaceID = aWindow.getWorkspaceID(); -- if (workspaceID) { -+ if (workspaceID && !(this.isLastRestorableWindow() && AppConstants.platform == "macosx")) { - winData.workspaceID = workspaceID; - } - }, -@@ -5625,11 +5634,12 @@ var SessionStoreInternal = { + // collect the data for all windows + for (ix in this._windows) { +- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) { ++ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) { + // window data is still in _statesToRestore + continue; + } +@@ -5625,11 +5650,16 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -90,19 +147,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 let winData = this._windows[aWindow.__SSi]; let tabsData = (winData.tabs = []); ++ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null; + winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore(); ++ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || []; ++ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces(); ++ // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5650,7 @@ var SessionStoreInternal = { - tabsData.push(tabData); - } - -+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || []; - // update tab group state for this window - winData.groups = []; - for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5663,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5682,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -111,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5765,8 +5776,8 @@ var SessionStoreInternal = { +@@ -5765,8 +5795,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -122,16 +175,17 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5809,6 +5820,8 @@ var SessionStoreInternal = { +@@ -5809,6 +5839,9 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); + aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders); + aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData); ++ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData); this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6372,6 +6385,25 @@ var SessionStoreInternal = { +@@ -6372,6 +6405,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -145,8 +199,8 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenPinnedId) { -+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); ++ if (tabData.zenSyncId) { ++ tab.setAttribute("id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -157,7 +211,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7290,7 +7322,7 @@ var SessionStoreInternal = { +@@ -7290,7 +7342,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { @@ -166,7 +220,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221 // Adjust window.selected if (tIndex + 1 < window.selected) { window.selected -= 1; -@@ -7305,7 +7337,7 @@ var SessionStoreInternal = { +@@ -7305,7 +7357,7 @@ var SessionStoreInternal = { ); // We don't want to increment tIndex here. continue; diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch index 2100e23343..450c981c32 100644 --- a/src/browser/components/sessionstore/TabState-sys-mjs.patch +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -1,13 +1,13 @@ diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs -index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644 +index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb1525c623 100644 --- a/browser/components/sessionstore/TabState.sys.mjs +++ b/browser/components/sessionstore/TabState.sys.mjs -@@ -85,7 +85,22 @@ class _TabState { +@@ -85,7 +85,24 @@ class _TabState { tabData.groupId = tab.group.id; } + tabData.zenWorkspace = tab.getAttribute("zen-workspace-id"); -+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id"); ++ tabData.zenSyncId = tab.getAttribute("id"); + tabData.zenEssential = tab.getAttribute("zen-essential"); + tabData.pinned = tabData.pinned || tabData.zenEssential; + tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId"); @@ -17,6 +17,8 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a + tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label"); + tabData.zenGlanceId = tab.getAttribute("glance-id"); + tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab"); ++ tabData._zenPinnedInitialState = tab._zenPinnedInitialState; ++ tabData._zenIsActiveTab = tab._zenContentsVisible; + tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true); + if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) { @@ -25,3 +27,12 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a tabData.userContextId = tab.userContextId || 0; +@@ -98,7 +115,7 @@ class _TabState { + + // Copy data from the tab state cache only if the tab has fully finished + // restoring. We don't want to overwrite data contained in __SS_data. +- this.copyFromCache(browser.permanentKey, tabData, options); ++ this.copyFromCache(tab.permanentKey, tabData, options); + + // After copyFromCache() was called we check for properties that are kept + // in the cache only while the tab is pending or restoring. Once that diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 3f27154b7f..dd0edd05f5 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js -index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644 +index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -121,15 +121,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf on_click(event) { if (event.button != 0) { return; -@@ -575,6 +599,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, -@@ -587,6 +612,14 @@ +@@ -587,6 +611,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -144,7 +136,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf } on_dblclick(event) { -@@ -610,6 +643,8 @@ +@@ -610,6 +642,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 8a71eaaae6..7327740e86 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 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f53bc059b 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18e829ca07 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f const browsers = []; if (this.#activeSplitView) { for (const tab of this.#activeSplitView.tabs) { -@@ -450,15 +451,64 @@ +@@ -450,15 +451,66 @@ return this.tabContainer.visibleTabs; } @@ -18,6 +18,8 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f + return this.#handleTabMove(...args); + } + ++ get zenTabProgressListener() { return TabProgressListener; } ++ + get _numVisiblePinTabsWithoutCollapsed() { + let i = 0; + for (let item of this.tabContainer.ariaFocusableItems) { @@ -77,7 +79,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -613,6 +663,7 @@ +@@ -613,6 +665,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -85,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -898,13 +949,17 @@ +@@ -898,13 +951,17 @@ } this.showTab(aTab); @@ -104,7 +106,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -917,11 +972,15 @@ +@@ -917,11 +974,15 @@ } this.#handleTabMove(aTab, () => { @@ -121,7 +123,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f }); aTab.style.marginInlineStart = ""; -@@ -1098,6 +1157,8 @@ +@@ -1098,6 +1159,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -130,7 +132,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1107,6 +1168,9 @@ +@@ -1107,6 +1170,9 @@ ); return; } @@ -140,7 +142,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1379,7 +1443,6 @@ +@@ -1379,7 +1445,6 @@ // Preview mode should not reset the owner if (!this._previewMode && !oldTab.selected) { @@ -148,7 +150,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); -@@ -1470,6 +1533,7 @@ +@@ -1470,6 +1535,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -156,7 +158,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1686,9 @@ +@@ -1622,6 +1688,9 @@ } let activeEl = document.activeElement; @@ -166,17 +168,20 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2012,8 @@ +@@ -1945,7 +2014,11 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { - if (!aLabel || aLabel.includes("about:reader?")) { ++ if (!aTab._zenContentsVisible && !aTab._zenChangeLabelFlag && !aTab._labelIsInitialTitle && !gZenWorkspaces.privateWindowOrDisabled) { ++ return false; ++ } + gZenPinnedTabManager.onTabLabelChanged(aTab); -+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) { ++ if (!aLabel || aLabel.includes("about:reader?") || (aTab.hasAttribute("zen-has-static-label") && !aTab._zenChangeLabelFlag)) { return false; } -@@ -2053,7 +2121,7 @@ +@@ -2053,7 +2126,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -185,7 +190,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2396,7 @@ +@@ -2328,6 +2401,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -193,7 +198,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2470,7 @@ +@@ -2401,8 +2475,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -203,7 +208,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f b.setAttribute("transparent", "true"); } -@@ -2567,7 +2635,7 @@ +@@ -2567,7 +2640,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -212,7 +217,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2694,8 @@ +@@ -2626,8 +2699,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -223,7 +228,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2882,6 @@ +@@ -2814,7 +2887,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -231,16 +236,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,8 @@ +@@ -2923,6 +2995,9 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, + _forZenEmptyTab, + essential, ++ zenWorkspaceId, } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3002,17 @@ +@@ -2933,10 +3008,17 @@ ); } @@ -258,7 +264,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3020,7 @@ +@@ -2944,6 +3026,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -266,14 +272,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3073,19 @@ +@@ -2996,6 +3079,21 @@ noInitialLabel, skipBackgroundNotify, }); + if (hasZenDefaultUserContextId) { + t.setAttribute("zenDefaultUserContextId", "true"); + } -+ if (zenForcedWorkspaceId !== undefined) { ++ if (zenWorkspaceId) { ++ t.setAttribute("zen-workspace-id", zenWorkspaceId); ++ } else if (zenForcedWorkspaceId !== undefined) { + t.setAttribute("zen-workspace-id", zenForcedWorkspaceId); + t.setAttribute("change-workspace", "") + } @@ -286,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3094,7 @@ +@@ -3004,6 +3102,7 @@ ownerTab, openerTab, pinned, @@ -294,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3113,7 @@ +@@ -3022,6 +3121,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -302,7 +310,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f })); if (focusUrlBar) { -@@ -3146,6 +3238,12 @@ +@@ -3146,6 +3246,12 @@ } } @@ -315,7 +323,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3349,10 +3447,10 @@ +@@ -3349,10 +3455,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -327,7 +335,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } if (!color) { -@@ -3373,9 +3471,14 @@ +@@ -3373,9 +3479,14 @@ label, isAdoptingGroup ); @@ -344,7 +352,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f ); group.addTabs(tabs); -@@ -3496,7 +3599,7 @@ +@@ -3496,7 +3607,7 @@ } this.#handleTabMove(tab, () => @@ -353,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f ); } -@@ -3698,6 +3801,7 @@ +@@ -3698,6 +3809,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -361,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3767,6 +3871,7 @@ +@@ -3767,6 +3879,7 @@ openWindowInfo, name, skipLoad, @@ -369,7 +377,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f }); } -@@ -3955,7 +4060,7 @@ +@@ -3955,7 +4068,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -378,7 +386,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f let url = "about:blank"; if (tabData.entries?.length) { -@@ -3992,8 +4097,10 @@ +@@ -3992,8 +4105,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -390,7 +398,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (select) { tabToSelect = tab; } -@@ -4005,7 +4112,8 @@ +@@ -4005,7 +4120,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -400,7 +408,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -4019,7 +4127,10 @@ +@@ -4019,7 +4135,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -412,7 +420,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f ); tabsFragment.appendChild(tabGroup.node); } -@@ -4064,9 +4175,23 @@ +@@ -4064,9 +4183,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -428,15 +436,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f + gZenWorkspaces._initialTab._shouldRemove = true; + } + } -+ } + } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; - } ++ } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4257,11 +4382,14 @@ +@@ -4257,11 +4390,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -452,7 +460,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if ( !bulkOrderedOpen && ((openerTab && -@@ -4273,7 +4401,7 @@ +@@ -4273,7 +4409,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -461,7 +469,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f tabGroup = previousTab.group; } if ( -@@ -4284,7 +4412,7 @@ +@@ -4284,7 +4420,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -470,7 +478,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4312,14 +4440,14 @@ +@@ -4312,14 +4448,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -489,7 +497,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4330,7 +4458,7 @@ +@@ -4330,7 +4466,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -498,7 +506,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4358,7 +4486,11 @@ +@@ -4358,7 +4494,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -510,7 +518,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } this._updateTabsAfterInsert(); -@@ -4366,6 +4498,7 @@ +@@ -4366,6 +4506,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -518,17 +526,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f TabBarVisibility.update(); } -@@ -4655,6 +4788,9 @@ - return; - } - -+ for (let tab of selectedTabs) { -+ gZenPinnedTabManager._removePinnedAttributes(tab, true); -+ } - this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); - } - -@@ -4916,6 +5052,7 @@ +@@ -4916,6 +5057,7 @@ telemetrySource, } = {} ) { @@ -536,7 +534,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -5005,6 +5142,7 @@ +@@ -5005,6 +5147,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -544,7 +542,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } catch (e) { console.error(e); } -@@ -5043,6 +5181,12 @@ +@@ -5043,6 +5186,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -557,7 +555,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5057,6 +5201,9 @@ +@@ -5057,6 +5206,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -567,7 +565,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5105,7 +5252,13 @@ +@@ -5105,7 +5257,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -582,7 +580,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f return; } -@@ -5239,7 +5392,7 @@ +@@ -5239,7 +5397,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -591,7 +589,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5263,6 +5416,7 @@ +@@ -5263,6 +5421,7 @@ newTab = true; } @@ -599,7 +597,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5303,13 +5457,7 @@ +@@ -5303,13 +5462,7 @@ aTab._mouseleave(); if (newTab) { @@ -614,7 +612,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } else { TabBarVisibility.update(); } -@@ -5442,6 +5590,7 @@ +@@ -5442,6 +5595,7 @@ this.tabs[i]._tPos = i; } @@ -622,7 +620,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5663,6 +5812,7 @@ +@@ -5663,6 +5817,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -630,7 +628,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5675,13 +5825,13 @@ +@@ -5675,13 +5830,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -646,7 +644,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5697,7 +5847,7 @@ +@@ -5697,7 +5852,7 @@ } if (tab) { @@ -655,7 +653,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } // If no qualifying visible tab was found, see if there is a tab in -@@ -5718,7 +5868,7 @@ +@@ -5718,7 +5873,7 @@ }); } @@ -664,7 +662,47 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } _blurTab(aTab) { -@@ -6124,10 +6274,10 @@ +@@ -5729,7 +5884,7 @@ + * @returns {boolean} + * False if swapping isn't permitted, true otherwise. + */ +- swapBrowsersAndCloseOther(aOurTab, aOtherTab) { ++ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) { + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if ( +@@ -5783,6 +5938,7 @@ + // fire the beforeunload event in the process. Close the other + // window if this was its last tab. + if ( ++ zenCloseOther && + !remoteBrowser._beginRemoveTab(aOtherTab, { + adoptedByTab: aOurTab, + closeWindowWithLastTab: true, +@@ -5794,7 +5950,7 @@ + // If this is the last tab of the window, hide the window + // immediately without animation before the docshell swap, to avoid + // about:blank being painted. +- let [closeWindow] = aOtherTab._endRemoveArgs; ++ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs; + if (closeWindow) { + let win = aOtherTab.ownerGlobal; + win.windowUtils.suppressAnimation(true); +@@ -5918,11 +6074,13 @@ + } + + // Finish tearing down the tab that's going away. ++ if (zenCloseOther) { + if (closeWindow) { + aOtherTab.ownerGlobal.close(); + } else { + remoteBrowser._endRemoveTab(aOtherTab); + } ++ } + + this.setTabTitle(aOurTab); + +@@ -6124,10 +6282,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -677,15 +715,33 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6186,6 +6336,7 @@ +@@ -6185,7 +6343,8 @@ + * * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ - replaceTabWithWindow(aTab, aOptions) { +- replaceTabWithWindow(aTab, aOptions) { ++ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) { + if (!this.isTab(aTab)) return; // TODO: Handle tab groups if (this.tabs.length == 1) { return null; } -@@ -6319,7 +6470,7 @@ +@@ -6209,12 +6368,14 @@ + } + + // tell a new window to take the "dropped" tab +- return window.openDialog( ++ let win = window.openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + options, + aTab + ); ++ win._zenStartupSyncFlag = zenForceSync ? 'synced' : 'unsynced'; ++ return win; + } + + /** +@@ -6319,7 +6480,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -694,7 +750,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } /** -@@ -6404,8 +6555,8 @@ +@@ -6404,8 +6565,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -705,7 +761,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6431,10 +6582,16 @@ +@@ -6431,10 +6592,16 @@ this.#handleTabMove( element, () => { @@ -724,7 +780,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6492,23 +6649,28 @@ +@@ -6492,23 +6659,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -759,7 +815,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } 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 -@@ -6521,14 +6683,34 @@ +@@ -6521,14 +6693,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. @@ -795,7 +851,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6537,7 +6719,7 @@ +@@ -6537,7 +6729,7 @@ element, () => { if (moveBefore) { @@ -804,7 +860,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f } else if (targetElement) { targetElement.after(element); } else { -@@ -6607,10 +6789,10 @@ +@@ -6607,10 +6799,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -817,7 +873,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6656,6 +6838,7 @@ +@@ -6656,6 +6848,7 @@ let state = { tabIndex: tab._tPos, @@ -825,7 +881,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6682,7 +6865,7 @@ +@@ -6682,7 +6875,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -834,7 +890,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6723,6 +6906,10 @@ +@@ -6723,6 +6916,10 @@ moveActionCallback(); @@ -845,7 +901,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7623,7 +7810,7 @@ +@@ -7623,7 +7820,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -854,7 +910,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f window.focus(); aEvent.preventDefault(); break; -@@ -7640,7 +7827,6 @@ +@@ -7640,7 +7837,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -862,7 +918,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f }); break; case "TabGroupCreateByUser": -@@ -8589,6 +8775,7 @@ +@@ -8589,6 +8785,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -870,7 +926,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9623,7 +9810,7 @@ var TabContextMenu = { +@@ -9623,7 +9820,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -879,11 +935,3 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9943,6 +10130,7 @@ var TabContextMenu = { - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); - gBrowser.removeTab(this.contextTab, { - animate: true, - ...gBrowser.TabMetrics.userTriggeredContext( diff --git a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch deleted file mode 100644 index 31608dbe08..0000000000 --- a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch +++ /dev/null @@ -1,44 +0,0 @@ -diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs -index 4db61038e5e476bad3a61dbdb707e5222c1f08f8..9eca13d9cfac3b762917aaaa942267effb743cf7 100644 ---- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs -+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs -@@ -45,11 +45,13 @@ function defaultQuery(conditions = "") { - let query = ` - SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.id, t.open_count, - ${lazy.PAGES_FRECENCY_FIELD} AS frecency, t.userContextId, -- h.last_visit_date, NULLIF(t.groupId, '') groupId -+ h.last_visit_date, NULLIF(t.groupId, '') groupId, zp.url AS pinned_url, zp.title AS pinned_title - FROM moz_places h - LEFT JOIN moz_openpages_temp t - ON t.url = h.url - AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL)) -+ LEFT JOIN zen_pins zp -+ ON zp.url = h.url - WHERE ( - (:switchTabsEnabled AND t.open_count > 0) OR - ${lazy.PAGES_FRECENCY_FIELD} <> 0 -@@ -63,7 +65,7 @@ function defaultQuery(conditions = "") { - :matchBehavior, :searchBehavior, NULL) - ELSE - AUTOCOMPLETE_MATCH(:searchString, h.url, -- h.title, '', -+ IFNULL(zp.title, h.title), '', - h.visit_count, h.typed, - 0, t.open_count, - :matchBehavior, :searchBehavior, NULL) -@@ -1176,11 +1178,13 @@ class Search { - ? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime() - : undefined; - let tabGroup = row.getResultByName("groupId"); -+ let pinnedTitle = row.getResultByIndex(12); -+ let pinnedUrl = row.getResultByIndex("pinned_url"); - - let match = { - placeId, -- value: url, -- comment: bookmarkTitle || historyTitle, -+ value: pinnedUrl || url, -+ comment: pinnedTitle || bookmarkTitle || historyTitle, - icon: UrlbarUtils.getIconForUrl(url), - frecency: frecency || FRECENCY_DEFAULT, - userContextId, diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest index 74c0232b85..0f38828a14 100644 --- a/src/zen/ZenComponents.manifest +++ b/src/zen/ZenComponents.manifest @@ -13,3 +13,4 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} #include common/Components.manifest +#include sessionstore/SessionComponents.manifest diff --git a/src/zen/common/modules/ZenCommonUtils.mjs b/src/zen/common/modules/ZenCommonUtils.mjs index e1ac284bc9..905939d8a1 100644 --- a/src/zen/common/modules/ZenCommonUtils.mjs +++ b/src/zen/common/modules/ZenCommonUtils.mjs @@ -38,6 +38,10 @@ export class nsZenMultiWindowFeature { if (!nsZenMultiWindowFeature.isActiveWindow) { return; } + return this.forEachWindow(callback); + } + + async forEachWindow(callback) { for (const browser of nsZenMultiWindowFeature.browsers) { try { if (browser.closed) continue; diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs index daa0d0962c..95b2aac202 100644 --- a/src/zen/common/modules/ZenSessionStore.mjs +++ b/src/zen/common/modules/ZenSessionStore.mjs @@ -17,8 +17,9 @@ class ZenSessionStore extends nsZenPreloadedFeature { if (tabData.zenWorkspace) { tab.setAttribute('zen-workspace-id', tabData.zenWorkspace); } - if (tabData.zenPinnedId) { - tab.setAttribute('zen-pin-id', tabData.zenPinnedId); + // Keep for now, for backward compatibility for window sync to work. + if (tabData.zenSyncId || tabData.zenPinnedId) { + tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId); } if (tabData.zenHasStaticLabel) { tab.setAttribute('zen-has-static-label', 'true'); @@ -32,6 +33,9 @@ class ZenSessionStore extends nsZenPreloadedFeature { if (tabData.zenPinnedEntry) { tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry); } + if (tabData._zenPinnedInitialState) { + tab._zenPinnedInitialState = tabData._zenPinnedInitialState; + } } async #waitAndCleanup() { diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs index 942dd27eef..d855312960 100644 --- a/src/zen/common/modules/ZenStartup.mjs +++ b/src/zen/common/modules/ZenStartup.mjs @@ -12,12 +12,7 @@ class ZenStartup { isReady = false; - async init() { - // important: We do this to ensure that some firefox components - // are initialized before we start our own initialization. - // please, do not remove this line and if you do, make sure to - // test the startup process. - await new Promise((resolve) => setTimeout(resolve, 0)); + init() { this.openWatermark(); this.#initBrowserBackground(); this.#changeSidebarLocation(); @@ -97,6 +92,7 @@ class ZenStartup { // Just in case we didn't get the right size. gZenUIManager.updateTabsToolbar(); this.closeWatermark(); + document.getElementById('tabbrowser-arrowscrollbox').setAttribute('orient', 'vertical'); this.isReady = true; }); } diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs index e16fc982f7..517696d30b 100644 --- a/src/zen/common/modules/ZenUIManager.mjs +++ b/src/zen/common/modules/ZenUIManager.mjs @@ -811,7 +811,6 @@ window.gZenVerticalTabsManager = { !aItem.isConnected || gZenUIManager.testingEnabled || !gZenStartup.isReady || - !gZenPinnedTabManager.hasInitializedPins || aItem.group?.hasAttribute('split-view-group') ) { return; @@ -1310,14 +1309,6 @@ window.gZenVerticalTabsManager = { } else { gBrowser.setTabTitle(this._tabEdited); } - if (this._tabEdited.getAttribute('zen-pin-id')) { - // Update pin title in storage - await gZenPinnedTabManager.updatePinTitle( - this._tabEdited, - this._tabEdited.label, - !!newName - ); - } // Maybe add some confetti here?!? gZenUIManager.motion.animate( diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css index a1715df59b..c75e00d2e2 100644 --- a/src/zen/common/styles/zen-browser-container.css +++ b/src/zen/common/styles/zen-browser-container.css @@ -60,3 +60,15 @@ } } } + +.zen-pseudo-browser-image { + position: absolute; + inset: 0; + opacity: 0.4; + pointer-events: none; + z-index: 2; +} + +browser[zen-pseudo-hidden='true'] { + -moz-subtree-hidden-only-visually: 1 !important; +} diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index 93f4f8808d..c1f40a074d 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -231,6 +231,13 @@ --toolbox-textcolor: currentColor !important; } + &[zen-unsynced-window='true'] { + --zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%); + --zen-main-browser-background-toolbar: var(--zen-main-browser-background); + --zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important; + --toolbox-textcolor: currentColor !important; + } + --toolbar-field-background-color: var(--zen-colors-input-bg) !important; --arrowpanel-background: var(--zen-dialog-background) !important; diff --git a/src/zen/common/sys/ZenCustomizableUI.sys.mjs b/src/zen/common/sys/ZenCustomizableUI.sys.mjs index bd7efc760d..42fd818dab 100644 --- a/src/zen/common/sys/ZenCustomizableUI.sys.mjs +++ b/src/zen/common/sys/ZenCustomizableUI.sys.mjs @@ -116,6 +116,9 @@ export const ZenCustomizableUI = new (class { #initCreateNewButton(window) { const button = window.document.getElementById('zen-create-new-button'); button.addEventListener('command', (event) => { + if (window.gZenWorkspaces.privateWindowOrDisabled) { + return window.document.getElementById('cmd_newNavigatorTab').doCommand(); + } if (button.hasAttribute('open')) { return; } diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index b7b6c00ad6..25cfe4b018 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -6,138 +6,135 @@ document.addEventListener( 'MozBeforeInitialXULLayout', () => { // defined in browser-sets.inc - document - .getElementById('zenCommandSet') - - .addEventListener('command', (event) => { - switch (event.target.id) { - case 'cmd_zenCompactModeToggle': - gZenCompactModeManager.toggle(); - break; - case 'cmd_zenCompactModeShowSidebar': - gZenCompactModeManager.toggleSidebar(); - break; - case 'cmd_toggleCompactModeIgnoreHover': - gZenCompactModeManager.toggle(true); - break; - case 'cmd_zenWorkspaceForward': - gZenWorkspaces.changeWorkspaceShortcut(); - break; - case 'cmd_zenWorkspaceBackward': - gZenWorkspaces.changeWorkspaceShortcut(-1); - break; - case 'cmd_zenSplitViewGrid': - gZenViewSplitter.toggleShortcut('grid'); - break; - case 'cmd_zenSplitViewVertical': - gZenViewSplitter.toggleShortcut('vsep'); - break; - case 'cmd_zenSplitViewHorizontal': - gZenViewSplitter.toggleShortcut('hsep'); - break; - case 'cmd_zenSplitViewUnsplit': - gZenViewSplitter.toggleShortcut('unsplit'); - break; - case 'cmd_zenSplitViewContextMenu': - gZenViewSplitter.contextSplitTabs(); - break; - case 'cmd_zenCopyCurrentURLMarkdown': - gZenCommonActions.copyCurrentURLAsMarkdownToClipboard(); - break; - case 'cmd_zenCopyCurrentURL': - gZenCommonActions.copyCurrentURLToClipboard(); - break; - case 'cmd_zenPinnedTabReset': - gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab); - break; - case 'cmd_zenPinnedTabResetNoTab': - gZenPinnedTabManager.resetPinnedTab(); - break; - case 'cmd_zenToggleSidebar': - gZenVerticalTabsManager.toggleExpand(); - break; - case 'cmd_zenOpenZenThemePicker': - gZenThemePicker.openThemePicker(event); - break; - case 'cmd_zenChangeWorkspaceTab': - gZenWorkspaces.changeTabWorkspace( - event.sourceEvent.target.getAttribute('zen-workspace-id') - ); - break; - case 'cmd_zenToggleTabsOnRight': - gZenVerticalTabsManager.toggleTabsOnRight(); - break; - case 'cmd_zenSplitViewLinkInNewTab': - gZenViewSplitter.splitLinkInNewTab(); - break; - case 'cmd_zenNewEmptySplit': - setTimeout(() => { - gZenViewSplitter.createEmptySplit(); - }, 0); - break; - case 'cmd_zenReplacePinnedUrlWithCurrent': - gZenPinnedTabManager.replacePinnedUrlWithCurrent(); - break; - case 'cmd_contextZenAddToEssentials': - gZenPinnedTabManager.addToEssentials(); - break; - case 'cmd_contextZenRemoveFromEssentials': - gZenPinnedTabManager.removeEssentials(); - break; - case 'cmd_zenCtxDeleteWorkspace': - gZenWorkspaces.contextDeleteWorkspace(event); - break; - case 'cmd_zenChangeWorkspaceName': - gZenVerticalTabsManager.renameTabStart({ - target: gZenWorkspaces.activeWorkspaceIndicator.querySelector( - '.zen-current-workspace-indicator-name' - ), - }); - break; - case 'cmd_zenChangeWorkspaceIcon': - gZenWorkspaces.changeWorkspaceIcon(); - break; - case 'cmd_zenReorderWorkspaces': - gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', { - timeout: 9000, - descriptionId: 'zen-workspaces-how-to-reorder-desc', - }); - break; - case 'cmd_zenOpenWorkspaceCreation': - gZenWorkspaces.openWorkspaceCreation(event); - break; - case 'cmd_zenOpenFolderCreation': - gZenFolders.createFolder([], { - renameFolder: true, - }); - break; - case 'cmd_zenTogglePinTab': { - const currentTab = gBrowser.selectedTab; - if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) { - if (currentTab.pinned) { - gBrowser.unpinTab(currentTab); - } else { - gBrowser.pinTab(currentTab); - } + document.getElementById('zenCommandSet').addEventListener('command', (event) => { + switch (event.target.id) { + case 'cmd_zenCompactModeToggle': + gZenCompactModeManager.toggle(); + break; + case 'cmd_zenCompactModeShowSidebar': + gZenCompactModeManager.toggleSidebar(); + break; + case 'cmd_toggleCompactModeIgnoreHover': + gZenCompactModeManager.toggle(true); + break; + case 'cmd_zenWorkspaceForward': + gZenWorkspaces.changeWorkspaceShortcut(); + break; + case 'cmd_zenWorkspaceBackward': + gZenWorkspaces.changeWorkspaceShortcut(-1); + break; + case 'cmd_zenSplitViewGrid': + gZenViewSplitter.toggleShortcut('grid'); + break; + case 'cmd_zenSplitViewVertical': + gZenViewSplitter.toggleShortcut('vsep'); + break; + case 'cmd_zenSplitViewHorizontal': + gZenViewSplitter.toggleShortcut('hsep'); + break; + case 'cmd_zenSplitViewUnsplit': + gZenViewSplitter.toggleShortcut('unsplit'); + break; + case 'cmd_zenSplitViewContextMenu': + gZenViewSplitter.contextSplitTabs(); + break; + case 'cmd_zenCopyCurrentURLMarkdown': + gZenCommonActions.copyCurrentURLAsMarkdownToClipboard(); + break; + case 'cmd_zenCopyCurrentURL': + gZenCommonActions.copyCurrentURLToClipboard(); + break; + case 'cmd_zenPinnedTabReset': + gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab); + break; + case 'cmd_zenPinnedTabResetNoTab': + gZenPinnedTabManager.resetPinnedTab(); + break; + case 'cmd_zenToggleSidebar': + gZenVerticalTabsManager.toggleExpand(); + break; + case 'cmd_zenOpenZenThemePicker': + gZenThemePicker.openThemePicker(event); + break; + case 'cmd_zenChangeWorkspaceTab': + gZenWorkspaces.changeTabWorkspace( + event.sourceEvent.target.getAttribute('zen-workspace-id') + ); + break; + case 'cmd_zenToggleTabsOnRight': + gZenVerticalTabsManager.toggleTabsOnRight(); + break; + case 'cmd_zenSplitViewLinkInNewTab': + gZenViewSplitter.splitLinkInNewTab(); + break; + case 'cmd_zenNewEmptySplit': + setTimeout(() => { + gZenViewSplitter.createEmptySplit(); + }, 0); + break; + case 'cmd_zenReplacePinnedUrlWithCurrent': + gZenPinnedTabManager.replacePinnedUrlWithCurrent(); + break; + case 'cmd_contextZenAddToEssentials': + gZenPinnedTabManager.addToEssentials(); + break; + case 'cmd_contextZenRemoveFromEssentials': + gZenPinnedTabManager.removeEssentials(); + break; + case 'cmd_zenCtxDeleteWorkspace': + gZenWorkspaces.contextDeleteWorkspace(event); + break; + case 'cmd_zenChangeWorkspaceName': + gZenVerticalTabsManager.renameTabStart({ + target: gZenWorkspaces.activeWorkspaceIndicator.querySelector( + '.zen-current-workspace-indicator-name' + ), + }); + break; + case 'cmd_zenChangeWorkspaceIcon': + gZenWorkspaces.changeWorkspaceIcon(); + break; + case 'cmd_zenReorderWorkspaces': + gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', { + timeout: 9000, + descriptionId: 'zen-workspaces-how-to-reorder-desc', + }); + break; + case 'cmd_zenOpenWorkspaceCreation': + gZenWorkspaces.openWorkspaceCreation(event); + break; + case 'cmd_zenOpenFolderCreation': + gZenFolders.createFolder([], { + renameFolder: true, + }); + break; + case 'cmd_zenTogglePinTab': { + const currentTab = gBrowser.selectedTab; + if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) { + if (currentTab.pinned) { + gBrowser.unpinTab(currentTab); + } else { + gBrowser.pinTab(currentTab); } - break; - } - case 'cmd_zenCloseUnpinnedTabs': - gZenWorkspaces.closeAllUnpinnedTabs(); - break; - case 'cmd_zenUnloadWorkspace': { - gZenWorkspaces.unloadWorkspace(); - break; } - default: - gZenGlanceManager.handleMainCommandSet(event); - if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) { - const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1; - gZenWorkspaces.shortcutSwitchTo(index); - } - break; + break; } - }); + case 'cmd_zenCloseUnpinnedTabs': + gZenWorkspaces.closeAllUnpinnedTabs(); + break; + case 'cmd_zenUnloadWorkspace': { + gZenWorkspaces.unloadWorkspace(); + break; + } + default: + gZenGlanceManager.handleMainCommandSet(event); + if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) { + const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1; + gZenWorkspaces.shortcutSwitchTo(index); + } + break; + } + }); }, { once: true } ); diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index 2209e67138..6c1d2dbc78 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -150,7 +150,6 @@ class ZenFolder extends MozTabbrowserTabGroup { for (let tab of this.allItems.reverse()) { tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; if (tab.hasAttribute('zen-empty-tab')) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); gBrowser.removeTab(tab); } else { gBrowser.ungroupTab(tab); @@ -160,7 +159,6 @@ class ZenFolder extends MozTabbrowserTabGroup { async delete() { for (const tab of this.allItemsRecursive) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); if (tab.hasAttribute('zen-empty-tab')) { // Manually remove the empty tabs as removeTabs() inside removeTabGroup // does ignore them. diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 8fdf63fec7..01d9f1062f 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -101,20 +101,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { .querySelector('menupopup'); changeFolderSpace.innerHTML = ''; for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) { - const item = document.createXULElement('menuitem'); - item.className = 'zen-workspace-context-menu-item'; - item.setAttribute('zen-workspace-id', workspace.uuid); - item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace); - let name = workspace.name; - const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); - if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { - name = `${workspace.icon} ${name}`; - } - item.setAttribute('label', name); - if (iconIsSvg) { - item.setAttribute('image', workspace.icon); - item.classList.add('zen-workspace-context-icon'); - } + const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); item.addEventListener('command', (event) => { if (!this.#lastFolderContextMenu) return; this.changeFolderToSpace( @@ -418,7 +405,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (selectedTab) { selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid); selectedTab.removeAttribute('folder-active'); - gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab; + gZenWorkspaces.lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab; } resolve(); }); @@ -434,10 +421,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature { tab.style.height = ''; } gBrowser.TabStateFlusher.flush(tab.linkedBrowser); - if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) { + if (gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) { // This tab is no longer the last selected tab in the previous workspace because it's being moved to // the current workspace - delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid]; + delete gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid]; } } } @@ -456,9 +443,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature { // we may encounter tab.group.setAttribute('zen-workspace-id', workspaceId); gBrowser.TabStateFlusher.flush(tab.linkedBrowser); - if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) { + if (gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId] === tab) { // This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace - delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId]; + delete gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId]; } } folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true })); @@ -506,9 +493,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature { tabs = [emptyTab, ...filteredTabs]; const folder = this._createFolderNode(options); - if (options.initialPinId) { - folder.setAttribute('zen-pin-id', options.initialPinId); - } if (options.insertAfter) { options.insertAfter.after(folder); @@ -860,7 +844,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { .open(group.icon, { onlySvgIcons: true }) .then((icon) => { this.setFolderUserIcon(group, icon); - group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true })); + group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true })); }) .catch((err) => { console.error(err); @@ -938,7 +922,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (!parentFolder && folder.hasAttribute('split-view-group')) continue; const emptyFolderTabs = folder.tabs .filter((tab) => tab.hasAttribute('zen-empty-tab')) - .map((tab) => tab.getAttribute('zen-pin-id')); + .map((tab) => tab.getAttribute('id')); let prevSiblingInfo = null; const prevSibling = folder.previousElementSibling; @@ -947,9 +931,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (prevSibling) { if (gBrowser.isTabGroup(prevSibling)) { prevSiblingInfo = { type: 'group', id: prevSibling.id }; - } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) { - const zenPinId = prevSibling.getAttribute('zen-pin-id'); - prevSiblingInfo = { type: 'tab', id: zenPinId }; + } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) { + prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') }; } else { prevSiblingInfo = { type: 'start', id: null }; } @@ -967,7 +950,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature { prevSiblingInfo: prevSiblingInfo, emptyTabIds: emptyFolderTabs, userIcon: userIcon?.getAttribute('href'), - pinId: folder.getAttribute('zen-pin-id'), // note: We shouldn't be using the workspace-id anywhere, we are just // remembering it for the pinned tabs manager to use it later. workspaceId: folder.getAttribute('zen-workspace-id'), @@ -994,10 +976,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature { tabFolderWorkingData.set(folderData.id, workingData); const oldGroup = document.getElementById(folderData.id); - folderData.emptyTabIds.forEach((zenPinId) => { - oldGroup - ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`) - ?.setAttribute('zen-empty-tab', true); + folderData.emptyTabIds.forEach((id) => { + oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true); }); if (oldGroup) { if (!folderData.splitViewGroup) { @@ -1009,7 +989,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { saveOnWindowClose: folderData.saveOnWindowClose, workspaceId: folderData.workspaceId, }); - folder.setAttribute('zen-pin-id', folderData.pinId); + folder.setAttribute('id', folderData.id); workingData.node = folder; oldGroup.before(folder); } else { @@ -1041,9 +1021,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (parentWorkingData && parentWorkingData.node) { switch (stateData?.prevSiblingInfo?.type) { case 'tab': { - const tab = parentWorkingData.node.querySelector( - `[zen-pin-id="${stateData.prevSiblingInfo.id}"]` - ); + const tab = document.getElementById(stateData.prevSiblingInfo.id); tab.after(node); break; } @@ -1153,7 +1131,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature { const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group; const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group'); - let firstGroupElem = dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling; + let firstGroupElem = + dropElementGroup?.querySelector('.zen-tab-group-start')?.nextElementSibling; if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement; const isInMiddleZone = diff --git a/src/zen/moz.build b/src/zen/moz.build index 21915a69ab..eb681597f0 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -13,4 +13,5 @@ DIRS += [ "tests", "urlbar", "toolkit", + "sessionstore", ] diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest new file mode 100644 index 0000000000..8e3b0f9b08 --- /dev/null +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +# Browser global components initializing before UI startup +category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init +category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init + +# App shutdown consumers +category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs new file mode 100644 index 0000000000..724087d98c --- /dev/null +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -0,0 +1,367 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +import { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs'; +import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', + SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', + setTimeout: 'resource://gre/modules/Timer.sys.mjs', +}); + +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true); + +// Note that changing this hidden pref will make the previous session file +// unused, causing a new session file to be created on next write. +const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); +const SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true); + +const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; +const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore'; + +/** + * Class representing the sidebar object stored in the session file. + * This object holds all the data related to tabs, groups, folders + * and split view state. + */ +class nsZenSidebarObject { + #sidebar = {}; + + get data() { + return Cu.cloneInto(this.#sidebar, {}); + } + + set data(data) { + this.#sidebar = data; + } +} + +export class nsZenSessionManager { + /** + * The JSON file instance used to read/write session data. + * @type {JSONFile} + */ + #file = null; + /** + * The sidebar object holding tabs, groups, folders and split view data. + * @type {nsZenSidebarObject} + */ + #sidebarObject = new nsZenSidebarObject(); + + // Called from SessionComponents.manifest on app-startup + init() { + let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path; + let backupFile = null; + if (SHOULD_BACKUP_FILE) { + backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME); + } + let filePath = PathUtils.join(profileDir, FILE_NAME); + this.#file = new JSONFile({ + path: filePath, + compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined, + backupFile, + }); + } + + log(...args) { + if (lazy.gShouldLog) { + console.info('ZenSessionManager:', ...args); + } + } + + /** + * Gets the spaces data from the Places database for migration. + * This is only called once during the first run after updating + * to a version that uses the new session manager. + */ + async #getSpacesFromDBForMigration() { + try { + const { PlacesUtils } = ChromeUtils.importESModule( + 'resource://gre/modules/PlacesUtils.sys.mjs' + ); + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached('SELECT * FROM zen_workspaces ORDER BY created_at ASC'); + this._migrationSpaceData = rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + name: row.getResultByName('name'), + icon: row.getResultByName('icon'), + containerTabId: row.getResultByName('container_id') ?? 0, + position: row.getResultByName('position'), + theme: row.getResultByName('theme_type') + ? { + type: row.getResultByName('theme_type'), + gradientColors: JSON.parse(row.getResultByName('theme_colors')), + opacity: row.getResultByName('theme_opacity'), + rotation: row.getResultByName('theme_rotation'), + texture: row.getResultByName('theme_texture'), + } + : null, + })); + } catch { + /* ignore errors during migration */ + } + } + + /** + * Reads the session file and populates the sidebar object. + * This should be only called once at startup. + * @see SessionFileInternal.read + */ + async readFile() { + try { + let promises = []; + promises.push(this.#file.load()); + if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { + promises.push(this.#getSpacesFromDBForMigration()); + } + await Promise.all(promises); + } catch (e) { + console.error('ZenSessionManager: Failed to read session file', e); + } + this.#sidebar = this.#file.data || {}; + } + + /** + * Called when the session file is read. Restores the sidebar data + * into all windows. + * + * @param initialState + * The initial session state read from the session file. + */ + onFileRead(initialState) { + // For the first time after migration, we restore the tabs + // That where going to be restored by SessionStore. The sidebar + // object will always be empty after migration because we haven't + // gotten the opportunity to save the session yet. + if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { + Services.prefs.setBoolPref(MIGRATION_PREF, true); + for (const winData of initialState.windows || []) { + winData.spaces = this._migrationSpaceData || []; + } + delete this._migrationSpaceData; + return; + } + // If there's no initial state, nothing to restore. This would + // happen if the file is empty or corrupted. + if (!initialState) { + return; + } + // If there are no windows, we create an empty one. By default, + // firefox would create simply a new empty window, but we want + // to make sure that the sidebar object is properly initialized. + // This would happen on first run after having a single private window + // open when quitting the app, for example. + if (!initialState.windows?.length) { + initialState.windows = [{}]; + } + // Restore all windows with the same sidebar object, this will + // guarantee that all tabs, groups, folders and split view data + // are properly synced across all windows. + this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`); + for (const winData of initialState.windows || []) { + this.#restoreWindowData(winData); + } + } + + get #sidebar() { + return this.#sidebarObject.data; + } + + set #sidebar(data) { + this.#sidebarObject.data = data; + } + + /** + * Saves the current session state. Collects data and writes to disk. + * + * @param state + * The current session state. + */ + saveState(state) { + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) { + // Don't save (or even collect) anything in permanent private + // browsing mode. We also don't want to save if there are no windows. + return; + } + this.#collectWindowData(state); + // This would save the data to disk asynchronously or when + // quitting the app. + this.#file.data = this.#sidebar; + this.#file.saveSoon(); + this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`); + } + + /** + * Saves the session data for a closed window if it meets the criteria. + * See SessionStoreInternal.maybeSaveClosedWindow for more details. + * + * @param aWinData - The window data object to save. + * @param isLastWindow - Whether this is the last saveable window. + */ + maybeSaveClosedWindow(aWinData, isLastWindow) { + // We only want to save the *last* normal window that is closed. + // If its not the last window, we can still update the sidebar object + // based on other open windows. + if (aWinData.isPopup || aWinData.isTaskbarTab || aWinData.isZenUnsynced || !isLastWindow) { + return; + } + this.log('Saving closed window session data into Zen session store'); + this.saveState({ windows: [aWinData] }); + } + + /** + * Collects session data for a given window. + * + * @param state + * The current session state. + */ + #collectWindowData(state) { + let sidebarData = this.#sidebar; + if (!sidebarData) { + sidebarData = {}; + } + + sidebarData.lastCollected = Date.now(); + this.#collectTabsData(sidebarData, state); + this.#sidebar = sidebarData; + } + + #filterUnusedTabs(tabs) { + return tabs.filter((tab) => { + // We need to ignore empty tabs with no group association + // as they are not useful to restore. + return !(tab.zenIsEmpty && !tab.groupId); + }); + } + + /** + * Collects session data for all tabs in a given window. + * + * @param sidebarData + * The sidebar data object to populate. + * @param state + * The current session state. + */ + #collectTabsData(sidebarData, state) { + const tabIdRelationMap = new Map(); + for (const window of state.windows) { + // Only accept the tabs with `_zenIsActiveTab` set to true from + // every window. We do this to avoid collecting tabs with invalid + // state when multiple windows are open. Note that if we a tab without + // this flag set in any other window, we just add it anyway. + for (const tabData of window.tabs) { + if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) { + tabIdRelationMap.set(tabData.zenSyncId, tabData); + } + } + } + + sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values())); + + sidebarData.folders = state.windows[0].folders; + sidebarData.splitViewData = state.windows[0].splitViewData; + sidebarData.groups = state.windows[0].groups; + sidebarData.spaces = state.windows[0].spaces; + } + + /** + * Restores the sidebar data into a given window data object. + * We do this in order to make sure all new window objects + * have the same sidebar data. + * + * @param aWindowData + * The window data object to restore into. + */ + #restoreWindowData(aWindowData) { + const sidebar = this.#sidebar; + if (!sidebar) { + return; + } + aWindowData.tabs = sidebar.tabs || []; + aWindowData.splitViewData = sidebar.splitViewData; + aWindowData.folders = sidebar.folders; + aWindowData.groups = sidebar.groups; + aWindowData.spaces = sidebar.spaces; + } + + /** + * Restores a new window with Zen session data. This should be called + * not at startup, but when a new window is opened by the user. + * + * @param aWindow + * The window to restore. + * @param SessionStoreInternal + * The SessionStore module instance. + * @param resolvePromise + * The promise resolver to call when done. We use a promise + * here because out workspace manager always waits for SessionStore + * to restore all the windows before initializing, but when opening + * a new window, that promise is always resolved, meaning it may run + * into a race condition if we try to restore the window synchronously + * here. + */ + restoreNewWindow(aWindow, SessionStoreInternal) { + if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { + return; + } + this.log('Restoring new window with Zen session data'); + const state = lazy.SessionStore.getCurrentState(true); + const windows = (state.windows || []).filter( + (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced + ); + let windowToClone = windows[0] || {}; + let newWindow = Cu.cloneInto(windowToClone, {}); + if (windows.length < 2) { + // We only want to restore the sidebar object if we found + // only one normal window to clone from (which is the one + // we are opening). + this.log('Restoring sidebar data into new window'); + this.#restoreWindowData(newWindow); + } + newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []); + + // These are window-specific from the previous window state that + // we don't want to restore into the new window. Otherwise, new + // windows would appear overlapping the previous one, or with + // the same size and position, which should be decided by the + // window manager. + delete newWindow.selected; + delete newWindow.screenX; + delete newWindow.screenY; + delete newWindow.width; + delete newWindow.height; + delete newWindow.sizemode; + delete newWindow.sizemodeBeforeMinimized; + delete newWindow.zIndex; + + const newState = { windows: [newWindow] }; + this.log(`Cloning window with ${newWindow.tabs.length} tabs`); + + SessionStoreInternal._deferredInitialState = newState; + SessionStoreInternal.initializeWindow(aWindow, newState); + } + + /** + * Gets the cloned spaces data from the sidebar object. + * This is used during migration to restore spaces into + * the initial session state. + * + * @returns {Array} The cloned spaces data. + */ + getClonedSpaces() { + const sidebar = this.#sidebar; + if (!sidebar || !sidebar.spaces) { + return []; + } + return Cu.cloneInto(sidebar.spaces, {}); + } +} + +export const ZenSessionStore = new nsZenSessionManager(); diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs new file mode 100644 index 0000000000..e0b0b79d9b --- /dev/null +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -0,0 +1,1031 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', + TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs', + ZenSessionStore: 'resource:///modules/zen/ZenSessionManager.sys.mjs', +}); + +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gWindowSyncEnabled', 'zen.window-sync.enabled'); +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.window-sync.log', true); + +const OBSERVING = ['browser-window-before-show']; +const INSTANT_EVENTS = ['SSWindowClosing']; +const EVENTS = [ + 'TabOpen', + 'TabClose', + + 'ZenTabIconChanged', + 'ZenTabLabelChanged', + + 'TabMove', + 'TabPinned', + 'TabUnpinned', + 'TabAddedToEssentials', + 'TabRemovedFromEssentials', + + 'TabGroupUpdate', + 'TabGroupCreate', + 'TabGroupRemoved', + 'TabGroupMoved', + + 'ZenTabRemovedFromSplit', + 'ZenSplitViewTabsSplit', + + 'TabSelect', + + 'focus', + ...INSTANT_EVENTS, +]; + +// Flags acting as an enum for sync types. +const SYNC_FLAG_LABEL = 1 << 0; +const SYNC_FLAG_ICON = 1 << 1; +const SYNC_FLAG_MOVE = 1 << 2; + +class nsZenWindowSync { + constructor() {} + + /** + * Context about the currently handled event. + * Used to avoid re-entrancy issues. + * + * We do still want to keep a stack of these in order + * to handle consecutive events properly. For example, + * loading a webpage will call IconChanged and TitleChanged + * events one after another. + */ + #eventHandlingContext = { + window: null, + eventCount: 0, + lastHandlerPromise: Promise.resolve(), + }; + + /** + * Last focused window. + * Used to determine which window to sync tab contents visibility from. + */ + #lastFocusedWindow = null; + + /** + * Last selected tab. + * Used to determine if we should run another sync operation + * when switching browser views. + */ + #lastSelectedTab = null; + + /** + * Iterator that yields all currently opened browser windows. + * (Might miss the most recent one.) + * This list is in focus order, but may include minimized windows + * before non-minimized windows. + */ + #browserWindows = { + *[Symbol.iterator]() { + for (let window of lazy.BrowserWindowTracker.orderedWindows) { + if ( + window.__SSi && + !window.closed && + window.gZenStartup.isReady && + !window.gZenWorkspaces?.privateWindowOrDisabled + ) { + yield window; + } + } + }, + }; + + init() { + if (!lazy.gWindowSyncEnabled) { + return; + } + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + uninit() { + for (let topic of OBSERVING) { + Services.obs.removeObserver(this, topic); + } + } + + log(...args) { + if (lazy.gShouldLog) { + console.info('ZenWindowSync:', ...args); + } + } + + /** + * Called when a browser window is about to be shown. + * Adds event listeners for the specified events. + * + * @param {Window} aWindow - The browser window that is about to be shown. + */ + #onWindowBeforeShow(aWindow) { + // There are 2 possibilities to know if we are trying to open + // a new *unsynced* window: + // 1. We are passing `zen-unsynced` in the window arguments. + // 2. We are trying to open a link in a new window where other synced + // windows already exist + let forcedSync = false; + let hasUnsyncedArg = false; + if (aWindow._zenStartupSyncFlag === 'synced') { + forcedSync = true; + } else if (aWindow._zenStartupSyncFlag === 'unsynced') { + hasUnsyncedArg = true; + } + delete aWindow._zenStartupSyncFlag; + if ( + !forcedSync && + (hasUnsyncedArg || + (typeof aWindow.arguments[0] === 'string' && + aWindow.arguments.length > 1 && + [...this.#browserWindows].length > 0)) + ) { + this.log('Not syncing new window due to unsynced argument or existing synced windows'); + aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true'); + return; + } + aWindow.gZenWindowSync = this; + for (let eventName of EVENTS) { + aWindow.addEventListener(eventName, this, true); + } + } + + /** + * @returns {string} A unique tab ID. + */ + get #newTabSyncId() { + // Note: If this changes, make sure to also update the + // getExtTabGroupIdForInternalTabGroupId implementation in + // browser/components/extensions/parent/ext-browser.js. + // See: Bug 1960104 - Improve tab group ID generation in addTabGroup + // This is implemented from gBrowser.addTabGroup. + return `${Date.now()}-${Math.round(Math.random() * 100)}`; + } + + /** + * Runs a callback function on all browser windows except the specified one. + * + * @param {Window} aWindow - The browser window to exclude. + * @param {Function} aCallback - The callback function to run on each window. + * @returns {any} The value returned by the callback function, if any. + */ + #runOnAllWindows(aWindow, aCallback) { + for (let window of this.#browserWindows) { + if (window !== aWindow && !window._zenClosingWindow) { + let value = aCallback(window); + if (value) { + return value; + } + } + } + return null; + } + + observe(aSubject, aTopic) { + switch (aTopic) { + case 'browser-window-before-show': { + this.#onWindowBeforeShow(aSubject); + break; + } + } + } + + handleEvent(aEvent) { + const window = aEvent.currentTarget.ownerGlobal; + if ( + !window.gZenStartup.isReady || + window.gZenWorkspaces?.privateWindowOrDisabled || + window._zenClosingWindow + ) { + return; + } + if (INSTANT_EVENTS.includes(aEvent.type)) { + return this.#handleNextEvent(aEvent); + } + if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) { + // We're already handling an event for another window. + // To avoid re-entrancy issues, we skip this event. + return; + } + const lastHandlerPromise = this.#eventHandlingContext.lastHandlerPromise; + this.#eventHandlingContext.eventCount++; + this.#eventHandlingContext.window = window; + let resolveNewPromise; + this.#eventHandlingContext.lastHandlerPromise = new Promise((resolve) => { + resolveNewPromise = resolve; + }); + // Wait for the last handler to finish before processing the next event. + lastHandlerPromise.then(() => { + this.#handleNextEvent(aEvent).finally(() => { + if (--this.#eventHandlingContext.eventCount === 0) { + this.#eventHandlingContext.window = null; + } + resolveNewPromise(); + }); + }); + } + + /** + * Handles the next event by calling the appropriate handler method. + * + * @param {Event} aEvent - The event to handle. + */ + #handleNextEvent(aEvent) { + const handler = `on_${aEvent.type}`; + try { + if (typeof this[handler] === 'function') { + return this[handler](aEvent) || Promise.resolve(); + } else { + throw new Error(`No handler for event type: ${aEvent.type}`); + } + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Ensures that all synced tabs with a given ID has the same permanentKey. + * @param {Object} aTab - The tab to ensure sync for. + */ + #makeSureTabSyncsPermanentKey(aTab) { + if (!aTab.id) { + return; + } + this.#runOnAllWindows(null, (win) => { + const tab = this.#getItemFromWindow(win, aTab.id); + if (tab) { + tab.permanentKey = aTab.linkedBrowser.permanentKey; + } + }); + } + + /** + * Retrieves a item element from a window by its ID. + * + * @param {Window} aWindow - The window containing the item. + * @param {string} aItemId - The ID of the item to retrieve. + * @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null. + */ + #getItemFromWindow(aWindow, aItemId) { + return aWindow.document.getElementById(aItemId); + } + + /** + * Synchronizes a specific attribute from the original item to the target item. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to. + * @param {string} aAttributeName - The name of the attribute to synchronize. + */ + #maybeSyncAttributeChange(aOriginalItem, aTargetItem, aAttributeName) { + if (aOriginalItem.hasAttribute(aAttributeName)) { + aTargetItem.setAttribute(aAttributeName, aOriginalItem.getAttribute(aAttributeName)); + } else { + aTargetItem.removeAttribute(aAttributeName); + } + } + + /** + * Synchronizes the icon and label of the target tab with the original tab. + * + * @param {Object} aOriginalTab - The original tab to copy from. + * @param {Object} aTargetTab - The target tab to copy to. + * @param {Window} aWindow - The window containing the tabs. + * @param {number} flags - The sync flags indicating what to synchronize. + */ + #syncItemWithOriginal(aOriginalItem, aTargetItem, aWindow, flags = 0) { + if (!aOriginalItem || !aTargetItem) { + return; + } + const { gBrowser, gZenFolders } = aWindow; + if (flags & SYNC_FLAG_ICON) { + if (gBrowser.isTab(aOriginalItem)) { + gBrowser.setIcon(aTargetItem, gBrowser.getIcon(aOriginalItem)); + } else if (aOriginalItem.isZenFolder) { + // Icons are a zen-only feature for tab groups. + gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL); + } + } + if (flags & SYNC_FLAG_LABEL) { + if (gBrowser.isTab(aOriginalItem)) { + aTargetItem._zenChangeLabelFlag = true; + gBrowser._setTabLabel(aTargetItem, aOriginalItem.label); + delete aTargetItem._zenChangeLabelFlag; + this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-has-static-label'); + } else if (gBrowser.isTabGroup(aOriginalItem)) { + aTargetItem.label = aOriginalItem.label; + } + } + if (flags & SYNC_FLAG_MOVE && !aTargetItem.hasAttribute('zen-empty-tab')) { + this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-workspace-id'); + this.#syncItemPosition(aOriginalItem, aTargetItem, aWindow); + } + if (gBrowser.isTab(aTargetItem)) { + lazy.TabStateFlusher.flush(aTargetItem.linkedBrowser); + } + } + + /** + * Synchronizes the position of the target item with the original item. + * + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to. + * @param {Window} aWindow - The window containing the items. + */ + #syncItemPosition(aOriginalItem, aTargetItem, aWindow) { + const { gBrowser, gZenPinnedTabManager } = aWindow; + const originalIsEssential = aOriginalItem.hasAttribute('zen-essential'); + const targetIsEssential = aTargetItem.hasAttribute('zen-essential'); + const originalIsPinned = aOriginalItem.pinned; + const targetIsPinned = aTargetItem.pinned; + + const isGroup = gBrowser.isTabGroup(aOriginalItem); + const isTab = !isGroup; + + if (isTab) { + if (originalIsEssential !== targetIsEssential) { + if (originalIsEssential) { + gZenPinnedTabManager.addToEssentials(aTargetItem); + } else { + gZenPinnedTabManager.removeEssentials(aTargetItem, /* unpin= */ !targetIsPinned); + } + } else if (originalIsPinned !== targetIsPinned) { + if (originalIsPinned) { + gBrowser.pinTab(aTargetItem); + } else { + gBrowser.unpinTab(aTargetItem); + } + } + } else { + aTargetItem.pinned = aOriginalItem.pinned; + } + + this.#moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { + isEssential: originalIsEssential, + isPinned: originalIsPinned, + }); + } + + /** + * Moves the target item to match the position of the original item. + * + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to match. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to move. + * @param {Window} aWindow - The window containing the items. + * @param {Object} options - Additional options for moving the item. + * @param {boolean} options.isEssential - Indicates if the item is essential. + * @param {boolean} options.isPinned - Indicates if the item is pinned. + */ + #moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { isEssential, isPinned }) { + const { gBrowser, gZenWorkspaces } = aWindow; + const originalSibling = aOriginalItem.previousElementSibling; + let isFirstTab = true; + if (gBrowser.isTabGroup(originalSibling) || gBrowser.isTab(originalSibling)) { + isFirstTab = + !originalSibling.hasAttribute('id') || originalSibling.hasAttribute('zen-empty-tab'); + } + + gBrowser.zenHandleTabMove(aOriginalItem, () => { + if (isFirstTab) { + let container; + const parentGroup = aOriginalItem.group; + if (parentGroup?.hasAttribute('id')) { + container = this.#getItemFromWindow(aWindow, parentGroup.getAttribute('id')); + if (container) { + if (container?.tabs?.length) { + // First tab in folders is the empty tab placeholder. + container.tabs[0].after(aTargetItem); + } else { + container.appendChild(aTargetItem); + } + return; + } + } + if (isEssential) { + container = gZenWorkspaces.getEssentialsSection(aTargetItem); + } else { + const workspaceId = + aTargetItem.getAttribute('zen-workspace-id') || + aOriginalItem.ownerGlobal.gZenWorkspaces.activeWorkspace; + const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); + container = isPinned + ? workspaceElement?.pinnedTabsContainer + : workspaceElement?.tabsContainer; + } + if (container) { + container.insertBefore(aTargetItem, container.firstChild); + } + return; + } + const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id); + if (relativeTab) { + relativeTab.after(aTargetItem); + } + }); + } + + /** + * Synchronizes a item across all browser windows. + * + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aItem - The item to synchronize. + * @param {number} flags - The sync flags indicating what to synchronize. + */ + #syncItemForAllWindows(aItem, flags = 0) { + const window = aItem.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + this.#syncItemWithOriginal(aItem, this.#getItemFromWindow(win, aItem.id), win, flags); + }); + } + + /** + * Swaps the browser docshells between two tabs. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + */ + async #swapBrowserDocShellsAsync(aOurTab, aOtherTab) { + await this.#styleSwapedBrowsers(aOurTab, aOtherTab, () => { + this.#swapBrowserDocSheellsInner(aOurTab, aOtherTab); + }); + } + + /** + * Restores the tab progress listener for a given tab. + * + * @param {Object} aTab - The tab to restore the progress listener for. + * @param {Function} callback - The callback function to execute while the listener is removed. + * @param {boolean} onClose - Indicates if the swap is done during a tab close operation. + */ + #withRestoreTabProgressListener(aTab, callback, onClose = false) { + const otherTabBrowser = aTab.ownerGlobal.gBrowser; + const otherBrowser = aTab.linkedBrowser; + + // We aren't closing the other tab so, we also need to swap its tablisteners. + let filter = otherTabBrowser._tabFilters.get(aTab); + let tabListener = otherTabBrowser._tabListeners.get(aTab); + try { + otherBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + } catch { + /* ignore errors, we might have already removed them */ + } + + try { + callback(); + } catch (e) { + console.error(e); + } + + // Restore the listeners for the swapped in tab. + if (!onClose) { + tabListener = new otherTabBrowser.zenTabProgressListener(aTab, otherBrowser, true, false); + otherTabBrowser._tabListeners.set(aTab, tabListener); + + const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; + filter.addProgressListener(tabListener, notifyAll); + otherBrowser.webProgress.addProgressListener(filter, notifyAll); + } + } + + /** + * Swaps the browser docshells between two tabs. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + * @param {boolean} focus - Indicates if the tab should be focused after the swap. + * @param {boolean} onClose - Indicates if the swap is done during a tab close operation. + */ + #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true, onClose = false) { + // Can't swap between chrome and content processes. + if (aOurTab.linkedBrowser.isRemoteBrowser != aOtherTab.linkedBrowser.isRemoteBrowser) { + return false; + } + // Load about:blank if by any chance we loaded the previous tab's URL. + // TODO: We should maybe start using a singular about:blank preloaded view + // to avoid loading a full blank page each time and wasting resources. + // We do need to do this though instead of just unloading the browser because + // firefox doesn't expect an unloaded + selected tab, so we need to get + // around this limitation somehow. + if (!onClose && aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { + this.log(`Loading about:blank in our tab ${aOurTab.id} before swap`); + aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, + }); + } + // Running `swapBrowsersAndCloseOther` doesn't expect us to use the tab after + // the operation, so it doesn't really care about cleaning up the other tab. + // We need to make a new tab progress listener for the other tab after the swap. + this.#withRestoreTabProgressListener( + aOtherTab, + () => { + this.log(`Swapping docshells between windows for tab ${aOurTab.id}`); + aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + this.#makeSureTabSyncsPermanentKey(aOurTab); + if (!aOtherTab.hasAttribute('busy')) { + aOurTab.removeAttribute('busy'); + } + }, + onClose + ); + const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture', 'busy']; + // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', + // but we need to manually remove some attributes from the other tab. + for (let attr of kAttributesToRemove) { + aOtherTab.removeAttribute(attr); + } + if (focus) { + // Recalculate the focus in order to allow the user to continue typing + // inside the web content area without having to click outside and back in. + aOurTab.linkedBrowser.blur(); + aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); + } + // Ensure the tab's state is flushed after the swap. By doing this, + // we can re-schedule another session store delayed process to fire. + // It's also important to note that if we don't flush the state here, + // we would start receiving invalid history changes from the the incorrect + // browser view that was just swapped out. + lazy.TabStateFlusher.flush(aOurTab.linkedBrowser); + return true; + } + + /** + * Styles the swapped browsers to ensure proper visibility and layout. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + * @param {Function|undefined} callback - The callback function to execute after styling. + */ + async #styleSwapedBrowsers(aOurTab, aOtherTab, callback = undefined) { + const ourBrowser = aOurTab.linkedBrowser; + const otherBrowser = aOtherTab.linkedBrowser; + + if (callback) { + const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob( + aOtherTab.linkedBrowser, + { + fullScale: true, + fullViewport: true, + } + ); + + let mySrc = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(browserBlob); + reader.onloadend = function () { + // result includes identifier 'data:image/png;base64,' plus the base64 data + resolve(reader.result); + }; + reader.onerror = function () { + reject(new Error('Failed to read blob as data URL')); + }; + }); + + const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc); + // Run a reflow to ensure the image is rendered before hiding the browser. + void img.getBoundingClientRect(); + await loadPromise; + otherBrowser.setAttribute('zen-pseudo-hidden', 'true'); + callback(); + } + + this.#maybeRemovePseudoImageForBrowser(ourBrowser); + ourBrowser.removeAttribute('zen-pseudo-hidden'); + } + + /** + * Create and insert a new pseudo image for a browser element. + * + * @param {Object} aBrowser - The browser element to create the pseudo image for. + * @param {string} aSrc - The source URL of the image. + * @returns {Object} The created pseudo image element. + */ + #createPseudoImageForBrowser(aBrowser, aSrc) { + const doc = aBrowser.ownerDocument; + const img = doc.createElement('img'); + img.className = 'zen-pseudo-browser-image'; + aBrowser.after(img); + const loadPromise = new Promise((resolve) => { + img.onload = () => resolve(); + img.src = aSrc; + }); + return [img, loadPromise]; + } + + /** + * Removes the pseudo image element for a browser if it exists. + * + * @param {Object} aBrowser - The browser element to remove the pseudo image for. + */ + #maybeRemovePseudoImageForBrowser(aBrowser) { + const elements = aBrowser.parentNode?.querySelectorAll('.zen-pseudo-browser-image'); + if (elements) { + elements.forEach((element) => element.remove()); + } + } + + /** + * Retrieves the active tab, where the web contents are being viewed + * from other windows by its ID. + * + * @param {Window} aWindow - The window to exclude. + * @param {string} aTabId - The ID of the tab to retrieve. + * @param {Function} filter - A function to filter the tabs. + * @returns {Object|null} The active tab from other windows if found, otherwise null. + */ + #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { + return this.#runOnAllWindows(aWindow, (win) => { + const tab = this.#getItemFromWindow(win, aTabId); + if (filter(tab)) { + return tab; + } + }); + } + + /** + * Moves all active tabs from the specified window to other windows. + * + * @param {Window} aWindow - The window to move active tabs from. + */ + #moveAllActiveTabsToOtherWindows(aWindow) { + const mostRecentWindow = [...this.#browserWindows].find((win) => win !== aWindow); + if (!mostRecentWindow || !aWindow.gZenWorkspaces) { + return; + } + const activeTabsOnClosedWindow = aWindow.gZenWorkspaces.allStoredTabs.filter( + (tab) => tab._zenContentsVisible + ); + for (let tab of activeTabsOnClosedWindow) { + const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id); + if (targetTab) { + targetTab._zenContentsVisible = true; + this.log(`Moving active tab ${tab.id} to most recent window on close`); + this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected, /* onClose =*/ true); + // We can animate later, whats important is to always stay on the same + // process and avoid async operations here to avoid the closed window + // being unloaded before the swap is done. + this.#styleSwapedBrowsers(targetTab, tab); + } + } + } + + /** + * Handles tab switch or window focus events to synchronize tab contents visibility. + * + * @param {Window} aWindow - The window that triggered the event. + * @param {Object} aPreviousTab - The previously selected tab. + * @param {boolean} ignoreSameTab - Indicates if the same tab should be ignored. + */ + async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null, ignoreSameTab = false) { + // On some occasions, such as when closing a window, this + // function might be called multiple times for the same tab. + if (aWindow.gBrowser.selectedTab === this.#lastSelectedTab && !ignoreSameTab) { + return; + } + if (aPreviousTab?._zenContentsVisible) { + const otherTabToShow = this.#getActiveTabFromOtherWindows( + aWindow, + aPreviousTab.id, + (tab) => tab?.selected + ); + if (otherTabToShow) { + otherTabToShow._zenContentsVisible = true; + delete aPreviousTab._zenContentsVisible; + await this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab); + } + } + 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); + } + + /** + * Delegates generic sync events to synchronize tabs across windows. + * + * @param {Event} aEvent - The event to delegate. + * @param {number} flags - The sync flags indicating what to synchronize. + */ + #delegateGenericSyncEvent(aEvent, flags = 0) { + const item = aEvent.target; + this.#syncItemForAllWindows(item, flags); + } + + /** + * Retrieves the tab state for a given tab. + * + * @param {Object} tab - The tab to retrieve the state for. + * @returns {Object} The tab state. + */ + #getTabState(tab) { + return JSON.parse(lazy.SessionStore.getTabState(tab)); + } + + /* Mark: Public API */ + + /** + * Sets the initial pinned state for a tab across all windows. + * + * @param {Object} aTab - The tab to set the pinned state for. + */ + setPinnedTabState(aTab) { + const state = this.#getTabState(aTab); + const initialState = { + entry: state.entries[state.index - 1], + image: state.image, + }; + this.#runOnAllWindows(null, (win) => { + const targetTab = this.#getItemFromWindow(win, aTab.id); + if (targetTab) { + targetTab._zenPinnedInitialState = initialState; + } + }); + } + + /** + * Propagates the workspaces to all windows. + * @param {Array} aWorkspaces - The workspaces to propagate. + */ + propagateWorkspacesToAllWindows(aWorkspaces) { + this.#runOnAllWindows(null, (win) => { + win.gZenWorkspaces.propagateWorkspaces(aWorkspaces); + }); + } + + /** + * Moves all tabs from a window to a synced workspace in another window. + * If no synced window exists, creates a new one. + * + * @param {Window} aWindow - The window to move tabs from. + * @param {string} aWorkspaceId - The ID of the workspace to move tabs to. + */ + moveTabsToSyncedWorkspace(aWindow, aWorkspaceId) { + const tabsToMove = aWindow.gZenWorkspaces.allStoredTabs.filter( + (tab) => !tab.hasAttribute('zen-empty-tab') + ); + const selectedTab = aWindow.gBrowser.selectedTab; + let win = [...this.#browserWindows][0]; + const moveAllTabsToWindow = async (allowSelected = false) => { + const { gBrowser, gZenWorkspaces } = win; + win.focus(); + let success = true; + for (const tab of tabsToMove) { + if (tab !== selectedTab || allowSelected) { + const newTab = gBrowser.adoptTab(tab, { tabIndex: Infinity }); + if (!newTab) { + // The adoption failed. Restore "fadein" and don't increase the index. + tab.setAttribute('fadein', 'true'); + success = false; + continue; + } + newTab._zenContentsVisible = true; + gBrowser.setTabTitle(newTab); + gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId); + } + } + if (success) { + aWindow.close(); + await gZenWorkspaces.changeWorkspaceWithID(aWorkspaceId); + gBrowser.selectedBrowser.focus(); + } + }; + if (!win) { + this.log('No synced window found, creating a new one'); + win = aWindow.gBrowser.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true); + win.gZenWorkspaces.promiseInitialized.then(() => { + moveAllTabsToWindow(); + }); + return; + } + moveAllTabsToWindow(true); + } + + /* Mark: Event Handlers */ + + on_TabOpen(aEvent) { + const tab = aEvent.target; + const window = tab.ownerGlobal; + if (tab.id) { + // This tab was opened as part of a sync operation. + return; + } + tab._zenContentsVisible = true; + tab.id = this.#newTabSyncId; + this.#runOnAllWindows(window, (win) => { + const newTab = win.gBrowser.addTrustedTab('about:blank', { + animate: true, + createLazyBrowser: true, + zenWorkspaceId: tab.getAttribute('zen-workspace-id') || '', + _forZenEmptyTab: tab.hasAttribute('zen-empty-tab'), + }); + newTab.id = tab.id; + this.#syncItemWithOriginal( + tab, + newTab, + win, + SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE + ); + }); + this.#makeSureTabSyncsPermanentKey(tab); + } + + on_ZenTabIconChanged(aEvent) { + if (!aEvent.target?._zenContentsVisible) { + // No need to sync icon changes for tabs that aren't active in this window. + return; + } + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON); + } + + on_ZenTabLabelChanged(aEvent) { + if (!aEvent.target?._zenContentsVisible) { + // No need to sync label changes for tabs that aren't active in this window. + return; + } + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL); + } + + on_TabMove(aEvent) { + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_MOVE); + } + + on_TabPinned(aEvent) { + const tab = aEvent.target; + // There are cases where the pinned state is changed but we don't + // wan't to override the initial state we stored when the tab was created. + // For example, when session restore pins a tab again. + if (!tab._zenPinnedInitialState) { + this.setPinnedTabState(tab); + } + return this.on_TabMove(aEvent); + } + + on_TabUnpinned(aEvent) { + const tab = aEvent.target; + this.#runOnAllWindows(null, (win) => { + const targetTab = this.#getItemFromWindow(win, tab.id); + if (targetTab) { + delete targetTab._zenPinnedInitialState; + } + }); + return this.on_TabMove(aEvent); + } + + on_TabAddedToEssentials(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabRemovedFromEssentials(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabClose(aEvent) { + const tab = aEvent.target; + const window = tab.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + const targetTab = this.#getItemFromWindow(win, tab.id); + if (targetTab) { + win.gBrowser.removeTab(targetTab, { animate: true }); + } + }); + } + + on_focus(aEvent) { + const { ownerGlobal: window } = aEvent.target; + if ( + !window.gBrowser || + this.#lastFocusedWindow?.deref() === window || + window.closing || + !window.toolbar.visible + ) { + return; + } + this.#lastFocusedWindow = new WeakRef(window); + this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab); + return this.#onTabSwitchOrWindowFocus(window); + } + + on_TabSelect(aEvent) { + const tab = aEvent.target; + if (this.#lastSelectedTab?.deref() === tab) { + return; + } + this.#lastSelectedTab = new WeakRef(tab); + const previousTab = aEvent.detail.previousTab; + return this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); + } + + on_SSWindowClosing(aEvent) { + const window = aEvent.target.ownerGlobal; + window._zenClosingWindow = true; + for (let eventName of EVENTS) { + window.removeEventListener(eventName, this); + } + delete window.gZenWindowSync; + this.#moveAllActiveTabsToOtherWindows(window); + } + + on_TabGroupCreate(aEvent) { + const tabGroup = aEvent.target; + if (tabGroup.id && tabGroup.alreadySynced) { + // This tab group was opened as part of a sync operation. + return; + } + 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([]); + newGroup.id = tabGroup.id; + newGroup.alreadySynced = true; + this.#syncItemWithOriginal( + tabGroup, + newGroup, + win, + SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE + ); + }); + } + + on_TabGroupRemoved(aEvent) { + const tabGroup = aEvent.target; + const window = tabGroup.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + const targetGroup = this.#getItemFromWindow(win, tabGroup.id); + if (targetGroup) { + if (targetGroup.isZenFolder) { + targetGroup.delete(); + } else { + win.gBrowser.removeTabGroup(targetGroup, { isUserTriggered: true }); + } + } + }); + } + + on_TabGroupMoved(aEvent) { + return this.on_TabMove(aEvent); + } + + 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', -1); + if (group) { + let otherTabGroup = group.tabs[0].group; + otherTabGroup.id = tabGroup.id; + this.#syncItemWithOriginal(aEvent.target, otherTabGroup, win, SYNC_FLAG_MOVE); + } + } + }); + + return this.#onTabSwitchOrWindowFocus(window, null, /* ignoreSameTab = */ true); + } +} + +export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build new file mode 100644 index 0000000000..188f4c27ce --- /dev/null +++ b/src/zen/sessionstore/moz.build @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +EXTRA_JS_MODULES.zen += [ + "ZenSessionManager.sys.mjs", + "ZenWindowSync.sys.mjs", +] diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 89488cd436..8fd03aa9ab 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) { @@ -1067,9 +1084,13 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { * * @param {Tab[]} tabs - The tabs to split. * @param {string|undefined} gridType - The type of grid layout. + * @param {number} initialIndex - The index of the initially active tab. + * use -1 to avoid selecting any tab. + * @return {object|undefined} The split view data or undefined if the split was not performed. */ splitTabs(tabs, gridType, initialIndex = 0) { - this.#withoutSplitViewTransition(() => { + const tabIndexToUse = Math.max(0, initialIndex); + 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) { @@ -1078,7 +1099,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { const existingSplitTab = tabs.find((tab) => tab.splitView); if (existingSplitTab) { - this._moveTabsToContainer(tabs, tabs[initialIndex]); + this._moveTabsToContainer(tabs, tabs[tabIndexToUse]); const groupIndex = this._data.findIndex((group) => group.tabs.includes(existingSplitTab)); const group = this._data[groupIndex]; const gridTypeChange = gridType && group.gridType !== gridType; @@ -1105,7 +1126,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 @@ -1132,8 +1154,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { layoutTree: this.calculateLayoutTree(tabs, gridType), }; this._data.push(splitData); - if (!this._sessionRestoring) { - window.gBrowser.selectedTab = tabs[initialIndex] ?? tabs[0]; + if (!this._sessionRestoring && initialIndex >= 0) { + window.gBrowser.selectedTab = tabs[tabIndexToUse] ?? tabs[0]; } // Add tabs to the split view group @@ -1150,6 +1172,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { return; } this.activateSplitView(splitData); + this.#dispatchItemEvent('ZenSplitViewTabsSplit', splitGroup); + return splitData; }); } @@ -1855,9 +1879,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 +1890,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { }); } - return null; + return group; } storeDataForSessionStore() { @@ -1936,7 +1961,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'); diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index c922d11663..182fa63ebf 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -7,23 +7,7 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components const lazy = {}; class ZenPinnedTabsObserver { - static ALL_EVENTS = [ - 'TabPinned', - 'TabUnpinned', - 'TabMove', - 'TabGroupCreate', - 'TabGroupRemoved', - 'TabGroupMoved', - 'ZenFolderRenamed', - 'ZenFolderIconChanged', - 'TabGroupCollapse', - 'TabGroupExpand', - 'TabGrouped', - 'TabUngrouped', - 'ZenFolderChangedWorkspace', - 'TabAddedToEssentials', - 'TabRemovedFromEssentials', - ]; + static ALL_EVENTS = ['TabPinned', 'TabUnpinned']; #listeners = []; @@ -76,12 +60,11 @@ class ZenPinnedTabsObserver { } class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { - hasInitializedPins = false; promiseInitializedPinned = new Promise((resolve) => { this._resolvePinnedInitializedInternal = resolve; }); - async init() { + init() { if (!this.enabled) { return; } @@ -103,43 +86,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } onTabIconChanged(tab, url = null) { + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); const iconUrl = url ?? tab.iconImage.src; - if (!iconUrl && tab.hasAttribute('zen-pin-id')) { - try { - setTimeout(async () => { - const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - if (favicon) { - gBrowser.setIcon(tab, favicon); - } - }); - } catch { - // Handle error - } - } else { - if (tab.hasAttribute('zen-essential')) { - tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); - } + if (tab.hasAttribute('zen-essential')) { + tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } } _onTabResetPinButton(event, tab) { event.stopPropagation(); - const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (!pin) { - return; - } - let userContextId; - if (tab.hasAttribute('usercontextid')) { - userContextId = tab.getAttribute('usercontextid'); - } - const pinnedUrl = Services.io.newURI(pin.url); - const browser = tab.linkedBrowser; - browser.loadURI(pinnedUrl, { - triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({ - userContextId, - }), - }); - this.resetPinChangedUrl(tab); + this._resetTabToStoredState(tab); } get enabled() { @@ -150,260 +106,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return lazy.zenTabsEssentialsMax; } - async refreshPinnedTabs({ init = false } = {}) { - if (!this.enabled) { - return; - } - await ZenPinnedTabsStorage.promiseInitialized; - await this.#initializePinsCache(); - setTimeout(async () => { - // Execute in a separate task to avoid blocking the main thread - await SessionStore.promiseAllWindowsRestored; - await gZenWorkspaces.promiseInitialized; - await this.#initializePinnedTabs(init); - if (init) { - this._hasFinishedLoading = true; - } - }, 100); - } - - async #initializePinsCache() { - try { - // Get pin data - const pins = await ZenPinnedTabsStorage.getPins(); - - // Enhance pins with favicons - this._pinsCache = await Promise.all( - pins.map(async (pin) => { - try { - if (pin.isGroup) { - return pin; // Skip groups for now - } - const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - return { - ...pin, - iconUrl: image || null, - }; - } catch { - // If favicon fetch fails, continue without icon - return { - ...pin, - iconUrl: null, - }; - } - }) - ); - } catch (ex) { - console.error('Failed to initialize pins cache:', ex); - this._pinsCache = []; - } - - this.log(`Initialized pins cache with ${this._pinsCache.length} pins`); - return this._pinsCache; - } - - #finishedInitializingPins() { - if (this.hasInitializedPins) { - return; - } - this._resolvePinnedInitializedInternal(); - delete this._resolvePinnedInitializedInternal; - this.hasInitializedPins = true; - } - - async #initializePinnedTabs(init = false) { - const pins = this._pinsCache; - if (!pins?.length || !init) { - this.#finishedInitializingPins(); - return; - } - - const pinnedTabsByUUID = new Map(); - const pinsToCreate = new Set(pins.map((p) => p.uuid)); - - // First pass: identify existing tabs and remove those without pins - for (let tab of gZenWorkspaces.allStoredTabs) { - const pinId = tab.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - - if (pinsToCreate.has(pinId)) { - // This is a valid pinned tab that matches a pin - pinnedTabsByUUID.set(pinId, tab); - pinsToCreate.delete(pinId); - - if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) { - this._resetTabToStoredState(tab); - } - } else { - // This is a pinned tab that no longer has a corresponding pin - gBrowser.removeTab(tab); - } - } - - for (const group of gZenWorkspaces.allTabGroups) { - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - if (pinsToCreate.has(pinId)) { - // This is a valid pinned group that matches a pin - pinsToCreate.delete(pinId); - } - } - - // Second pass: For every existing tab, update its label - // and set 'zen-has-static-label' attribute if it's been edited - for (let pin of pins) { - const tab = pinnedTabsByUUID.get(pin.uuid); - if (!tab) { - continue; - } - - tab.removeAttribute('zen-has-static-label'); // So we can set it again - if (pin.title && pin.editedTitle) { - gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true }); - tab.setAttribute('zen-has-static-label', 'true'); - } - } - - const groups = new Map(); - const pendingTabsInsideGroups = {}; - - // Third pass: create new tabs for pins that don't have tabs - for (let pin of pins) { - try { - if (!pinsToCreate.has(pin.uuid)) { - continue; // Skip pins that already have tabs - } - - if (pin.isGroup) { - const tabs = []; - // If there's already existing tabs, let's use them - for (const [uuid, existingTab] of pinnedTabsByUUID) { - const pinObject = this._pinsCache.find((p) => p.uuid === uuid); - if (pinObject && pinObject.parentUuid === pin.uuid) { - tabs.push(existingTab); - } - } - // We still need to iterate through pending tabs since the database - // query doesn't guarantee the order of insertion - for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) { - if (parentUuid === pin.uuid) { - tabs.push(...folderTabs); - } - } - const group = gZenFolders.createFolder(tabs, { - label: pin.title, - collapsed: pin.isFolderCollapsed, - initialPinId: pin.uuid, - workspaceId: pin.workspaceUuid, - insertAfter: - groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null, - }); - gZenFolders.setFolderUserIcon(group, pin.folderIcon); - groups.set(pin.uuid, group); - continue; - } - - let params = { - skipAnimation: true, - allowInheritPrincipal: false, - skipBackgroundNotify: true, - userContextId: pin.containerTabId || 0, - createLazyBrowser: true, - skipLoad: true, - noInitialLabel: false, - }; - - // Create and initialize the tab - let newTab = gBrowser.addTrustedTab(pin.url, params); - newTab.setAttribute('zenDefaultUserContextId', true); - - // Set initial label/title - if (pin.title) { - gBrowser.setInitialTabTitle(newTab, pin.title); - } - - // Set the icon if we have it cached - if (pin.iconUrl) { - gBrowser.setIcon(newTab, pin.iconUrl); - } - - newTab.setAttribute('zen-pin-id', pin.uuid); - - if (pin.workspaceUuid) { - newTab.setAttribute('zen-workspace-id', pin.workspaceUuid); - } - - if (pin.isEssential) { - newTab.setAttribute('zen-essential', 'true'); - } - - if (pin.editedTitle) { - newTab.setAttribute('zen-has-static-label', 'true'); - } - - // Initialize browser state if needed - if (!newTab.linkedBrowser._remoteAutoRemoved) { - let state = { - entries: [ - { - url: pin.url, - title: pin.title, - triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, - }, - ], - userContextId: pin.containerTabId || 0, - image: pin.iconUrl, - }; - - SessionStore.setTabState(newTab, state); - } - - this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`); - gBrowser.pinTab(newTab); - - if (pin.parentUuid) { - const parentGroup = groups.get(pin.parentUuid); - if (parentGroup) { - parentGroup.querySelector('.tab-group-container').appendChild(newTab); - } else { - if (pendingTabsInsideGroups[pin.parentUuid]) { - pendingTabsInsideGroups[pin.parentUuid].push(newTab); - } else { - pendingTabsInsideGroups[pin.parentUuid] = [newTab]; - } - } - } else { - if (!pin.isEssential) { - const container = gZenWorkspaces.workspaceElement( - pin.workspaceUuid - )?.pinnedTabsContainer; - if (container) { - container.insertBefore(newTab, container.lastChild); - } - } else { - gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab); - } - } - - gBrowser.tabContainer._invalidateCachedTabs(); - newTab.initialize(); - } catch (ex) { - console.error('Failed to initialize pinned tabs:', ex); - } - } - - setTimeout(() => { - this.#finishedInitializingPins(); - }, 0); - - gBrowser._updateTabBarForPinnedTabs(); - gZenUIManager.updateTabsToolbar(); - } - _onPinnedTabEvent(action, event) { if (!this.enabled) return; const tab = event.target; @@ -413,236 +115,24 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } switch (action) { case 'TabPinned': - case 'TabAddedToEssentials': tab._zenClickEventListener = this._zenClickEventListener; tab.addEventListener('click', tab._zenClickEventListener); - this._setPinnedAttributes(tab); break; - case 'TabRemovedFromEssentials': - if (tab.pinned) { - this.#onTabMove(tab); - break; - } // [Fall through] case 'TabUnpinned': - this._removePinnedAttributes(tab); if (tab._zenClickEventListener) { tab.removeEventListener('click', tab._zenClickEventListener); delete tab._zenClickEventListener; } break; - case 'TabMove': - this.#onTabMove(tab); - break; - case 'TabGroupCreate': - this.#onTabGroupCreate(event); - break; - case 'TabGroupRemoved': - this.#onTabGroupRemoved(event); - break; - case 'TabGroupMoved': - this.#onTabGroupMoved(event); - break; - case 'ZenFolderRenamed': - case 'ZenFolderIconChanged': - case 'TabGroupCollapse': - case 'TabGroupExpand': - case 'ZenFolderChangedWorkspace': - this.#updateGroupInfo(event.originalTarget, action); - break; - case 'TabGrouped': - this.#onTabGrouped(event); - break; - case 'TabUngrouped': - this.#onTabUngrouped(event); - break; default: console.warn('ZenPinnedTabManager: Unhandled tab event', action); break; } } - async #onTabGroupCreate(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - if (group.hasAttribute('zen-pin-id')) { - return; // Group already exists in storage - } - const workspaceId = group.getAttribute('zen-workspace-id'); - let id = await ZenPinnedTabsStorage.createGroup( - group.name, - group.iconURL, - group.collapsed, - workspaceId, - group.getAttribute('zen-pin-id'), - group._pPos - ); - group.setAttribute('zen-pin-id', id); - for (const tab of group.tabs) { - // Only add it if the tab is directly under the group - if ( - tab.pinned && - tab.hasAttribute('zen-pin-id') && - tab.group === group && - this.hasInitializedPins - ) { - const tabPinId = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos); - } - } - await this.refreshPinnedTabs(); - } - - async #onTabGrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin || !tabPin.group) { - return; - } - ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos); - } - - async #onTabUngrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group?.isZenFolder) { - return; - } - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin) { - return; - } - ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos); - } - - async #updateGroupInfo(group, action) { - if (!group?.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.title = group.name; - groupPin.folderIcon = group.iconURL; - groupPin.isFolderCollapsed = group.collapsed; - groupPin.position = group._pPos; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null; - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null; - await this.savePin(groupPin); - switch (action) { - case 'ZenFolderRenamed': - case 'ZenFolderIconChanged': - case 'TabGroupCollapse': - case 'TabGroupExpand': - break; - default: - for (const item of group.allItems) { - if (gBrowser.isTabGroup(item)) { - await this.#updateGroupInfo(item, action); - } else { - await this.#onTabMove(item); - } - } - } - } - } - - async #onTabGroupRemoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id')); - group.removeAttribute('zen-pin-id'); - } - - async #onTabGroupMoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - const newIndex = group._pPos; - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - return; - } - for (const tab of group.allItemsRecursive) { - if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) { - const pin = this._pinsCache.find((p) => p.uuid === pinId); - if (pin) { - pin.position = tab._pPos; - pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - pin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(pin, false); - } - break; - } - } - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.position = newIndex; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id'); - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(groupPin); - } - } - - async #onTabMove(tab) { - if (!tab.pinned || !this._pinsCache) { - return; - } - - const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups]; - for (let i = 0; i < allTabs.length; i++) { - const otherTab = allTabs[i]; - if ( - otherTab.pinned && - otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id') - ) { - const actualPin = this._pinsCache.find( - (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id') - ); - if (!actualPin) { - continue; - } - actualPin.position = otherTab._pPos; - actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id'); - actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null; - await this.savePin(actualPin, false); - } - } - - const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!actualPin) { - return; - } - actualPin.position = tab._pPos; - actualPin.isEssential = tab.hasAttribute('zen-essential'); - actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null; - - // There was a bug where the title and hasStaticLabel attribute were not being set - // This is a workaround to fix that - if (tab.hasAttribute('zen-has-static-label')) { - actualPin.editedTitle = true; - actualPin.title = tab.label; - } - await this.savePin(actualPin); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabMoved', { - detail: { tab }, - }) - ); + #getTabState(tab) { + return JSON.parse(SessionStore.getTabState(tab)); } async _onTabClick(e) { @@ -668,110 +158,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { async replacePinnedUrlWithCurrent(tab = undefined) { tab ??= TabContextMenu.contextTab; - if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) { - return; - } - - const browser = tab.linkedBrowser; - - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!pin) { + if (!tab || !tab.pinned) { return; } - const userContextId = tab.getAttribute('usercontextid'); - - pin.title = tab.label || browser.contentTitle; - pin.url = browser.currentURI.spec; - pin.workspaceUuid = tab.getAttribute('zen-workspace-id'); - pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0; - - await this.savePin(pin); + window.gZenWindowSync.setPinnedTabState(tab); this.resetPinChangedUrl(tab); - await this.refreshPinnedTabs(); gZenUIManager.showToast('zen-pinned-tab-replaced'); } - async _setPinnedAttributes(tab) { - if ( - tab.hasAttribute('zen-pin-id') || - !this._hasFinishedLoading || - tab.hasAttribute('zen-empty-tab') - ) { - return; - } - - this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`); - const browser = tab.linkedBrowser; - - const uuid = gZenUIManager.generateUuidv4(); - const userContextId = tab.getAttribute('usercontextid'); - - let entry = null; - - if (tab.getAttribute('zen-pinned-entry')) { - entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); - } - - await this.savePin({ - uuid, - title: entry?.title || tab.label || browser.contentTitle, - url: entry?.url || browser.currentURI.spec, - containerTabId: userContextId ? parseInt(userContextId, 10) : 0, - workspaceUuid: tab.getAttribute('zen-workspace-id'), - isEssential: tab.getAttribute('zen-essential') === 'true', - parentUuid: tab.group?.getAttribute('zen-pin-id') || null, - position: tab._pPos, - }); - - tab.setAttribute('zen-pin-id', uuid); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabCreated', { - detail: { tab }, - }) - ); - - // This is used while migrating old pins to new system - we don't want to refresh when migrating - if (tab.getAttribute('zen-pinned-entry')) { - tab.removeAttribute('zen-pinned-entry'); - return; - } - this.onLocationChange(browser); - await this.refreshPinnedTabs(); - } - - async _removePinnedAttributes(tab, isClosing = false) { - tab.removeAttribute('zen-has-static-label'); - if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) { - return; - } - - if (Services.startup.shuttingDown || window.skipNextCanClose) { - return; - } - - this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`); - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - this.resetPinChangedUrl(tab); - - if (!isClosing) { - tab.removeAttribute('zen-pin-id'); - tab.removeAttribute('zen-essential'); // Just in case - - if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) { - const workspace = await gZenWorkspaces.getActiveWorkspace(); - tab.setAttribute('zen-workspace-id', workspace.uuid); - } - } - await this.refreshPinnedTabs(); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabRemoved', { - detail: { tab }, - }) - ); - } - _initClosePinnedTabShortcut() { let cmdClose = document.getElementById('cmd_close'); @@ -780,21 +175,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async savePin(pin, notifyObservers = true) { - if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) { - return; - } - const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid); - if (existingPin) { - Object.assign(existingPin, pin); - } else { - // We shouldn't need it, but just in case there's - // a race condition while making new pinned tabs. - this._pinsCache.push(pin); - } - await ZenPinnedTabsStorage.savePin(pin, notifyObservers); - } - async onCloseTabShortcut( event, selectedTab = gBrowser.selectedTab, @@ -841,7 +221,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { switch (behavior) { case 'close': { for (const tab of pinnedTabs) { - this._removePinnedAttributes(tab, true); gBrowser.removeTab(tab, { animate: true }); } break; @@ -943,35 +322,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } _resetTabToStoredState(tab) { - const id = tab.getAttribute('zen-pin-id'); - if (!id) { - return; - } + const state = this.#getTabState(tab); - const pin = this._pinsCache.find((pin) => pin.uuid === id); - if (!pin) { - return; - } + const initialState = tab._zenPinnedInitialState; - const tabState = SessionStore.getTabState(tab); - const state = JSON.parse(tabState); - - const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url); - if (foundEntryIndex === -1) { - state.entries = [ - { - url: pin.url, - title: pin.title, - triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, - }, - ]; - } else { - // Remove everything except the entry we want to keep - const existingEntry = state.entries[foundEntryIndex]; - existingEntry.title = pin.title; - state.entries = [existingEntry]; - } - state.image = pin.iconUrl || state.image; + // Remove everything except the entry we want to keep + state.entries = [initialState.entry]; + + state.image = initialState.image; state.index = 0; SessionStore.setTabState(tab, state); @@ -1016,22 +374,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { if (tab.hasAttribute('zen-workspace-id')) { tab.removeAttribute('zen-workspace-id'); } - if (tab.pinned && tab.hasAttribute('zen-pin-id')) { - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (pin) { - pin.isEssential = true; - pin.workspaceUuid = null; - this.savePin(pin); - } + if (tab.pinned) { gBrowser.zenHandleTabMove(tab, () => { if (tab.ownerGlobal !== window) { tab = gBrowser.adoptTab(tab, { selectTab: tab.selected, }); tab.setAttribute('zen-essential', 'true'); - } else { - section.appendChild(tab); } + section.appendChild(tab); }); } else { gBrowser.pinTab(tab); @@ -1124,8 +475,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } const isVisible = contextTab.pinned && !contextTab.multiselected; const zenAddEssential = document.getElementById('context_zen-add-essential'); - document.getElementById('context_zen-reset-pinned-tab').hidden = - !isVisible || !contextTab.getAttribute('zen-pin-id'); + document.getElementById('context_zen-reset-pinned-tab').hidden = !isVisible; document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible; zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group; zenAddEssential.setAttribute( @@ -1261,24 +611,25 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async onLocationChange(browser) { + onLocationChange(browser) { const tab = gBrowser.getTabForBrowser(browser); - if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) { - return; - } - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (!pin) { + if ( + !tab || + !tab.pinned || + tab.hasAttribute('zen-essential') || + !tab._zenPinnedInitialState?.entry + ) { return; } // Remove # and ? from the URL - const pinUrl = pin.url.split('#')[0]; + const pinUrl = tab._zenPinnedInitialState.entry.url.split('#')[0]; const currentUrl = browser.currentURI.spec.split('#')[0]; // Add an indicator that the pin has been changed if (pinUrl === currentUrl) { this.resetPinChangedUrl(tab); return; } - this.pinHasChangedUrl(tab, pin); + this.pinHasChangedUrl(tab); } resetPinChangedUrl(tab) { @@ -1290,7 +641,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { tab.style.removeProperty('--zen-original-tab-icon'); } - pinHasChangedUrl(tab, pin) { + pinHasChangedUrl(tab) { if (tab.hasAttribute('zen-pinned-changed')) { return; } @@ -1299,7 +650,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } else { tab.setAttribute('zen-pinned-changed', 'true'); } - tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`); + tab.style.setProperty('--zen-original-tab-icon', `url(${tab._zenPinnedInitialState.image})`); } removeTabContainersDragoverClass(hideIndicator = true) { @@ -1415,39 +766,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true'; } - async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) { - const uuid = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers); - - await this.refreshPinnedTabs(); - - const browsers = Services.wm.getEnumerator('navigator:browser'); - - // update the label for the same pin across all windows - for (const browser of browsers) { - const tabs = browser.gBrowser.tabs; - // Fix pinned cache for the browser - const browserCache = browser.gZenPinnedTabManager?._pinsCache; - if (browserCache) { - const pin = browserCache.find((pin) => pin.uuid === uuid); - if (pin) { - pin.title = newTitle; - pin.editedTitle = isEdited; - } - } - for (let i = 0; i < tabs.length; i++) { - const tabToEdit = tabs[i]; - if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) { - tabToEdit.removeAttribute('zen-has-static-label'); - if (isEdited) { - gBrowser._setTabLabel(tabToEdit, newTitle); - tabToEdit.setAttribute('zen-has-static-label', 'true'); - } else { - gBrowser.setTabTitle(tabToEdit); - } - break; - } - } + async updatePinTitle(tab, newTitle, isEdited = true) { + tab.removeAttribute('zen-has-static-label'); + if (isEdited) { + gBrowser._setTabLabel(tab, newTitle); + tab.setAttribute('zen-has-static-label', 'true'); + } else { + gBrowser.setTabTitle(tab); } } @@ -1563,19 +888,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async onTabLabelChanged(tab) { - if (!this._pinsCache) { - return; - } - // If our current pin in the cache point to about:blank, we need to update the entry - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (!pin) { - return; - } - - if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') { - await this.replacePinnedUrlWithCurrent(tab); - } + onTabLabelChanged(tab) { + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } })); } } diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs deleted file mode 100644 index a407dad353..0000000000 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ /dev/null @@ -1,660 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// 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/. - -window.ZenPinnedTabsStorage = { - _saveCache: [], - - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { - // Create the pins table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - is_essential BOOLEAN NOT NULL DEFAULT 0, - is_group BOOLEAN NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - const columns = await db.execute(`PRAGMA table_info(zen_pins)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); - } - }; - - await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); - await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) - `); - - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) - `); - - this._resolveInitialized(); - }); - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyPinsChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async savePin(pin, notifyObservers = true) { - // If we find the exact same pin in the cache, skip saving - const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid); - const copy = { ...pin }; - if (existingIndex !== -1) { - const existingPin = this._saveCache[existingIndex]; - const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]); - if (isSame) { - return; // No changes, skip saving - } else { - // Update the cached pin - this._saveCache[existingIndex] = { ...copy }; - } - } else { - // Add to cache - this._saveCache.push(copy); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in pin && Number.isFinite(pin.position)) { - newPosition = pin.position; - } else { - // Get the maximum position within the same parent group (or null for root level) - const maxPositionResult = await db.execute( - ` - SELECT MAX("position") as max_position - FROM zen_pins - WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') - `, - { folder_parent_uuid: pin.parentUuid || null } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - // Insert or replace the pin - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_pins ( - uuid, title, url, container_id, workspace_uuid, position, - is_essential, is_group, folder_parent_uuid, edited_title, created_at, - updated_at, is_folder_collapsed, folder_icon - ) VALUES ( - :uuid, :title, :url, :container_id, :workspace_uuid, :position, - :is_essential, :is_group, :folder_parent_uuid, :edited_title, - COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), - :now, :is_folder_collapsed, :folder_icon - ) - `, - { - uuid: pin.uuid, - title: pin.title, - url: pin.isGroup ? '' : pin.url, - container_id: pin.containerTabId || null, - workspace_uuid: pin.workspaceUuid || null, - position: newPosition, - is_essential: pin.isEssential || false, - is_group: pin.isGroup || false, - folder_parent_uuid: pin.parentUuid || null, - edited_title: pin.editedTitle || false, - now, - folder_icon: pin.folderIcon || null, - is_folder_collapsed: pin.isFolderCollapsed || false, - } - ); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async getPins() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_pins - ORDER BY position ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - title: row.getResultByName('title'), - url: row.getResultByName('url'), - containerTabId: row.getResultByName('container_id'), - workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position'), - isEssential: Boolean(row.getResultByName('is_essential')), - isGroup: Boolean(row.getResultByName('is_group')), - parentUuid: row.getResultByName('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - })); - }, - - /** - * Create a new group - * @param {string} title - The title of the group - * @param {string} workspaceUuid - The workspace UUID (optional) - * @param {string} parentUuid - The parent group UUID (optional, null for root level) - * @param {number} position - The position of the group (optional, will auto-calculate if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - * @returns {Promise} The UUID of the created group - */ - async createGroup( - title, - icon = null, - isCollapsed = false, - workspaceUuid = null, - parentUuid = null, - position = null, - notifyObservers = true - ) { - if (!title || typeof title !== 'string') { - throw new Error('Group title is required and must be a string'); - } - - const groupUuid = gZenUIManager.generateUuidv4(); - - const groupPin = { - uuid: groupUuid, - title, - folderIcon: icon || null, - isFolderCollapsed: isCollapsed || false, - workspaceUuid, - parentUuid, - position, - isGroup: true, - isEssential: false, - editedTitle: true, // Group titles are always considered edited - }; - - await this.savePin(groupPin, notifyObservers); - return groupUuid; - }, - - /** - * Add an existing tab/pin to a group - * @param {string} tabUuid - The UUID of the tab to add to the group - * @param {string} groupUuid - The UUID of the target group - * @param {number} position - The position within the group (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { - if (!tabUuid || !groupUuid) { - throw new Error('Both tabUuid and groupUuid are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { - await db.executeTransaction(async () => { - // Verify the group exists and is actually a group - const groupCheck = await db.execute( - `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, - { groupUuid } - ); - - if (groupCheck.length === 0) { - throw new Error(`Group with UUID ${groupUuid} does not exist`); - } - - if (!groupCheck[0].getResultByName('is_group')) { - throw new Error(`Pin with UUID ${groupUuid} is not a group`); - } - - const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { - tabUuid, - }); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - const now = Date.now(); - let newPosition; - - if (position !== null && Number.isFinite(position)) { - newPosition = position; - } else { - // Get the maximum position within the group - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, - { groupUuid } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = :groupUuid, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - groupUuid, - newPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - /** - * Remove a tab from its group (move to root level) - * @param {string} tabUuid - The UUID of the tab to remove from its group - * @param {number} newPosition - The new position at root level (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { - if (!tabUuid) { - throw new Error('tabUuid is required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.removeTabFromGroup', - async (db) => { - await db.executeTransaction(async () => { - // Verify the tab exists and is in a group - const tabCheck = await db.execute( - `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, - { tabUuid } - ); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - if (!tabCheck[0].getResultByName('folder_parent_uuid')) { - return; - } - - const now = Date.now(); - let finalPosition; - - if (newPosition !== null && Number.isFinite(newPosition)) { - finalPosition = newPosition; - } else { - // Get the maximum position at root level (where folder_parent_uuid is null) - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - finalPosition = maxPosition + 1000; - } - - // Update the tab to be at root level - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = NULL, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - newPosition: finalPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async removePin(uuid, notifyObservers = true) { - const cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid); - if (cachedIndex !== -1) { - this._saveCache.splice(cachedIndex, 1); - } - - const changedUUIDs = [uuid]; - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.executeTransaction(async () => { - // Get all child UUIDs first for change tracking - const children = await db.execute( - `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, - { - uuid, - } - ); - - // Add child UUIDs to changedUUIDs array - for (const child of children) { - changedUUIDs.push(child.getResultByName('uuid')); - } - - // Delete the pin/group itself - await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); - - // Record the changes - const now = Math.floor(Date.now() / 1000); - for (const changedUuid of changedUUIDs) { - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: changedUuid, - timestamp: now, - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-removed', changedUUIDs); - } - }, - - async wipeAllPins() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { - await db.execute(`DELETE FROM zen_pins`); - await db.execute(`DELETE FROM zen_pins_changes`); - await this.updateLastChangeTimestamp(db); - }); - }, - - async markChanged(uuid) { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - }); - }, - - async getChangedIDs() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_pins_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { - await db.execute(`DELETE FROM zen_pins_changes`); - }); - }, - - shouldReorderPins(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllPins(db, changedUUIDs) { - const pins = await db.execute(` - SELECT uuid - FROM zen_pins - ORDER BY position ASC - `); - - for (let i = 0; i < pins.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pins[i].getResultByName('uuid') } - ); - changedUUIDs.add(pins[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_pins_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updatePinPositions(pins) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.updatePinPositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < pins.length; i++) { - const pin = pins[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pin.uuid } - ); - - changedUUIDs.add(pin.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - }, - - async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { - if (!uuid || typeof newTitle !== 'string') { - throw new Error('Invalid parameters: uuid and newTitle are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - // Update the pin's title and edited_title flag - const result = await db.execute( - ` - UPDATE zen_pins - SET title = :newTitle, - edited_title = :isEdited, - updated_at = :now - WHERE uuid = :uuid - `, - { - uuid, - newTitle, - isEdited, - now, - } - ); - - // Only proceed with change tracking if a row was actually updated - if (result.rowsAffected > 0) { - changedUUIDs.add(uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - }); - }); - - if (notifyObservers && changedUUIDs.size > 0) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async __dropTables() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { - await db.execute(`DROP TABLE IF EXISTS zen_pins`); - await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); - }); - }, -}; - -ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { - ZenPinnedTabsStorage._resolveInitialized = resolve; - ZenPinnedTabsStorage.init(); -}); diff --git a/src/zen/tabs/jar.inc.mn b/src/zen/tabs/jar.inc.mn index 2acab4816b..192d0d33d2 100644 --- a/src/zen/tabs/jar.inc.mn +++ b/src/zen/tabs/jar.inc.mn @@ -2,7 +2,6 @@ # 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/. - content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs) content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs) * content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css) content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css) \ No newline at end of file diff --git a/src/zen/tests/container_essentials/browser_container_auto_switch.js b/src/zen/tests/container_essentials/browser_container_auto_switch.js index b46279668b..1e9a94826c 100644 --- a/src/zen/tests/container_essentials/browser_container_auto_switch.js +++ b/src/zen/tests/container_essentials/browser_container_auto_switch.js @@ -6,7 +6,7 @@ add_task(async function test_Container_Essentials_Auto_Swithc() { await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', { skipAnimation: true, @@ -27,11 +27,11 @@ add_task(async function test_Container_Essentials_Auto_Swithc() { const newWorkspaceUUID = gZenWorkspaces.activeWorkspace; Assert.equal( gZenWorkspaces.activeWorkspace, - workspaces.workspaces[1].uuid, + workspaces[1].uuid, 'The new workspace should be active.' ); // Change to the original workspace, there should be no essential tabs - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]); + await gZenWorkspaces.changeWorkspace(workspaces[0]); await gZenWorkspaces.removeWorkspace(newWorkspaceUUID); }); diff --git a/src/zen/tests/container_essentials/browser_container_specific_essentials.js b/src/zen/tests/container_essentials/browser_container_specific_essentials.js index a376c01651..3d0b82ec84 100644 --- a/src/zen/tests/container_essentials/browser_container_specific_essentials.js +++ b/src/zen/tests/container_essentials/browser_container_specific_essentials.js @@ -6,9 +6,9 @@ add_task(async function test_Check_Creation() { await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[1]); + await gZenWorkspaces.changeWorkspace(workspaces[1]); let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', { skipAnimation: true, userContextId: 1, @@ -28,7 +28,7 @@ add_task(async function test_Check_Creation() { const newWorkspaceUUID = gZenWorkspaces.activeWorkspace; // Change to the original workspace, there should be no essential tabs - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]); + await gZenWorkspaces.changeWorkspace(workspaces[0]); ok( !gBrowser.tabs.find( (t) => t.hasAttribute('zen-essential') && t.getAttribute('usercontextid') == 1 diff --git a/src/zen/tests/folders/browser_folder_owner_tabs.js b/src/zen/tests/folders/browser_folder_owner_tabs.js index 00de0f44a7..7af7c963a5 100644 --- a/src/zen/tests/folders/browser_folder_owner_tabs.js +++ b/src/zen/tests/folders/browser_folder_owner_tabs.js @@ -31,9 +31,6 @@ add_task(async function test_Duplicate_Tab_Inside_Folder() { for (const t of folder.tabs) { ok(t.pinned, 'All tabs in the folder should be pinned'); - if (!t.hasAttribute('zen-empty-tab')) { - ok(t.hasAttribute('zen-pin-id'), 'All non-empty tabs should have a zen-pinned-id attribute'); - } } gBrowser.selectedTab = selectedTab; diff --git a/src/zen/tests/pinned/browser.toml b/src/zen/tests/pinned/browser.toml index b26d96f321..22082e1f5f 100644 --- a/src/zen/tests/pinned/browser.toml +++ b/src/zen/tests/pinned/browser.toml @@ -13,14 +13,9 @@ prefs = ["zen.workspaces.separate-essentials=false"] ["browser_pinned_close.js"] ["browser_pinned_changed.js"] ["browser_pinned_created.js"] -["browser_pinned_edit_label.js"] -["browser_pinned_removed.js"] -["browser_pinned_reorder_changed_label.js"] -["browser_pinned_reordered.js"] ["browser_pinned_to_essential.js"] ["browser_private_mode_no_essentials.js"] ["browser_private_mode_no_ctx_menu.js"] -["browser_issue_7654.js"] ["browser_issue_8726.js"] diff --git a/src/zen/tests/pinned/browser_issue_7654.js b/src/zen/tests/pinned/browser_issue_7654.js deleted file mode 100644 index d982a910f4..0000000000 --- a/src/zen/tests/pinned/browser_issue_7654.js +++ /dev/null @@ -1,65 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -ChromeUtils.defineESModuleGetters(this, { - UrlbarTestUtils: 'resource://testing-common/UrlbarTestUtils.sys.mjs', -}); - -add_task(async function test_Search_Pinned_Title() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const customLabel = 'ZEN ROCKS'; - - await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { - const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function () { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true); - - const pinnedTabs = await ZenPinnedTabsStorage.getPins(); - const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID); - Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title'); - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/2', true); - - await UrlbarTestUtils.promiseAutocompleteResultPopup({ - window, - value: customLabel, - waitForFocus: SimpleTest.waitForFocus, - }); - - const total = UrlbarTestUtils.getResultCount(window); - info(`Found ${total} matches`); - - const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); - - const url = result?.url; - Assert.equal( - url, - 'https://example.com/1', - `Should have the found result '${url}' in the expected list of entries` - ); - Assert.equal( - result?.title, - customLabel, - `Should have the found result '${result?.title}' in the expected list of entries` - ); - - BrowserTestUtils.removeTab(gBrowser.selectedTab); - resolvePromise(); - }, - { once: true } - ); - gBrowser.pinTab(tab); - await promise; - }); -}); diff --git a/src/zen/tests/pinned/browser_pinned_changed.js b/src/zen/tests/pinned/browser_pinned_changed.js index 576c16946d..f2ffb466fa 100644 --- a/src/zen/tests/pinned/browser_pinned_changed.js +++ b/src/zen/tests/pinned/browser_pinned_changed.js @@ -11,27 +11,20 @@ add_task(async function test_Changed_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); + gBrowser.pinTab(tab); - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); + ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); + + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + resolvePromise(); + }, 0); - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - resolvePromise(); - }, 0); - }, - { once: true } - ); - gBrowser.pinTab(tab); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_close.js b/src/zen/tests/pinned/browser_pinned_close.js index c3c009ae2b..b283a8c10b 100644 --- a/src/zen/tests/pinned/browser_pinned_close.js +++ b/src/zen/tests/pinned/browser_pinned_close.js @@ -15,20 +15,13 @@ add_task(async function test_Unload_NoReset_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok(tab.closing, 'The tab should be closing after being closed'); - resolvePromise(); - }, 100); - }, - { once: true } - ); gBrowser.pinTab(tab); + + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok(tab.closing, 'The tab should be closing after being closed'); + resolvePromise(); + }, 100); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_created.js b/src/zen/tests/pinned/browser_pinned_created.js index 6a8070aa15..bc7658fcbc 100644 --- a/src/zen/tests/pinned/browser_pinned_created.js +++ b/src/zen/tests/pinned/browser_pinned_created.js @@ -12,38 +12,24 @@ add_task(async function test_Create_Pinned() { await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - try { - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage'); - Assert.equal( - pinObject.url, - 'https://example.com/', - 'The pin object should have the correct URL' - ); - Assert.equal( - pinObject.workspaceUuid, - gZenWorkspaces.activeWorkspace, - 'The pin object should have the correct workspace UUID' - ); - } catch (error) { - ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error); - } - - resolvePromise(); - }, - { once: true } - ); gBrowser.pinTab(newTab); + ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); + + try { + const pinObject = newTab.__zenPinnedInitialState; + ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage'); + Assert.equal( + pinObject.entry.url, + 'https://example.com/', + 'The pin object should have the correct URL' + ); + } catch (error) { + ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error); + } + + resolvePromise(); + await promise; await BrowserTestUtils.removeTab(newTab); }); diff --git a/src/zen/tests/pinned/browser_pinned_edit_label.js b/src/zen/tests/pinned/browser_pinned_edit_label.js deleted file mode 100644 index e03cb7cb87..0000000000 --- a/src/zen/tests/pinned/browser_pinned_edit_label.js +++ /dev/null @@ -1,42 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -add_task(async function test_Create_Pinned() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const customLabel = 'Test Label'; - - await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/' }, async (browser) => { - const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true); - - const pinnedTabs = await ZenPinnedTabsStorage.getPins(); - const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID); - Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title'); - Assert.equal( - pinObject.url, - 'https://example.com/', - 'The pin object should have the correct URL' - ); - - resolvePromise(); - }, - { once: true } - ); - gBrowser.pinTab(tab); - await promise; - }); -}); diff --git a/src/zen/tests/pinned/browser_pinned_nounload_reset.js b/src/zen/tests/pinned/browser_pinned_nounload_reset.js index 9930b02681..8effbee1f4 100644 --- a/src/zen/tests/pinned/browser_pinned_nounload_reset.js +++ b/src/zen/tests/pinned/browser_pinned_nounload_reset.js @@ -15,37 +15,26 @@ add_task(async function test_NoUnload_Changed_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok( - !tab.hasAttribute('zen-pinned-changed'), - 'The tab should not have a zen-pinned-changed attribute after being closed' - ); - ok( - !tab.hasAttribute('discarded'), - 'The tab should not be discarded after being closed' - ); - ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); - resolvePromise(); - }, 100); - }, 0); - }, - { once: true } - ); gBrowser.pinTab(tab); + + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok( + !tab.hasAttribute('zen-pinned-changed'), + 'The tab should not have a zen-pinned-changed attribute after being closed' + ); + ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); + ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); + resolvePromise(); + }, 100); + }, 0); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_removed.js b/src/zen/tests/pinned/browser_pinned_removed.js deleted file mode 100644 index 4e4bc79599..0000000000 --- a/src/zen/tests/pinned/browser_pinned_removed.js +++ /dev/null @@ -1,52 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -add_task(async function test_Remove_Pinned() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - - const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage'); - newTab.addEventListener( - 'ZenPinnedTabRemoved', - async function (event) { - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - ok( - !pinObject, - 'The pin object should not exist in the ZenPinnedTabsStorage after removal' - ); - ok( - !newTab.hasAttribute('zen-pin-id'), - 'The tab should not have a zen-pin-id attribute after removal' - ); - ok(!newTab.pinned, 'The tab should not be pinned after removal'); - resolvePromise(); - }, - { once: true } - ); - gBrowser.unpinTab(newTab); - }, - { once: true } - ); - gBrowser.pinTab(newTab); - - await promise; - await BrowserTestUtils.removeTab(newTab); -}); diff --git a/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js b/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js deleted file mode 100644 index 10908c565f..0000000000 --- a/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js +++ /dev/null @@ -1,126 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -add_task(async function test_Pinned_Reorder_Changed_Label() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const tabsToRemove = []; - for (let i = 0; i < 3; i++) { - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - gBrowser.pinTab(gBrowser.selectedTab); - tabsToRemove.push(gBrowser.selectedTab); - } - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - tabsToRemove.push(gBrowser.selectedTab); - - const customLabel = 'Test Label'; - - const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true); - - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - - newTab.addEventListener( - 'ZenPinnedTabMoved', - async function (event) { - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - Assert.equal( - pinObject.title, - customLabel, - 'The pin object should have the correct title' - ); - Assert.equal( - pinObject.position, - 2, - 'The pin object should have the correct position after moving' - ); - resolvePromise(); - }, - { once: true } - ); - gBrowser.moveTabTo(newTab, { tabIndex: 2 }); - }, - { once: true } - ); - gBrowser.pinTab(newTab); - - await promise; - for (const tab of tabsToRemove) { - await BrowserTestUtils.removeTab(tab); - } -}); - -add_task(async function test_Pinned_Reorder_Changed_Label() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const tabsToRemove = []; - for (let i = 0; i < 3; i++) { - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - gBrowser.pinTab(gBrowser.selectedTab); - tabsToRemove.push(gBrowser.selectedTab); - } - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - tabsToRemove.push(gBrowser.selectedTab); - - const customLabel = 'Test Label'; - - const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - newTab.addEventListener( - 'ZenPinnedTabMoved', - async function (event) { - await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true); - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - Assert.equal( - pinObject.title, - customLabel, - 'The pin object should have the correct title' - ); - Assert.equal( - pinObject.position, - 1, - 'The pin object should have the correct position after moving' - ); - resolvePromise(); - }, - { once: true } - ); - gBrowser.moveTabTo(newTab, { tabIndex: 1 }); - }, - { once: true } - ); - gBrowser.pinTab(newTab); - - await promise; - for (const tab of tabsToRemove) { - await BrowserTestUtils.removeTab(tab); - } -}); diff --git a/src/zen/tests/pinned/browser_pinned_reordered.js b/src/zen/tests/pinned/browser_pinned_reordered.js deleted file mode 100644 index 6fc523b8bd..0000000000 --- a/src/zen/tests/pinned/browser_pinned_reordered.js +++ /dev/null @@ -1,97 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -add_task(async function test_Create_Pinned() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const tabsToRemove = []; - for (let i = 0; i < 3; i++) { - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - gBrowser.pinTab(gBrowser.selectedTab); - tabsToRemove.push(gBrowser.selectedTab); - } - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - tabsToRemove.push(gBrowser.selectedTab); - - const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - const startIndex = pinObject.position; - Assert.greater(startIndex, 0, 'The pin object should have the correct start index'); - - resolvePromise(); - }, - { once: true } - ); - gBrowser.pinTab(newTab); - - await promise; - for (const tab of tabsToRemove) { - await BrowserTestUtils.removeTab(tab); - } -}); - -add_task(async function test_Create_Pinned() { - let resolvePromise; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - const tabsToRemove = []; - for (let i = 0; i < 3; i++) { - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - gBrowser.pinTab(gBrowser.selectedTab); - tabsToRemove.push(gBrowser.selectedTab); - } - - await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); - tabsToRemove.push(gBrowser.selectedTab); - - const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - newTab.addEventListener( - 'ZenPinnedTabMoved', - async function (event) { - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - Assert.equal( - pinObject.position, - 0, - 'The pin object should have the correct position after moving' - ); - resolvePromise(); - }, - { once: true } - ); - gBrowser.moveTabTo(newTab, { tabIndex: 0 }); - }, - { once: true } - ); - gBrowser.pinTab(newTab); - - await promise; - for (const tab of tabsToRemove) { - await BrowserTestUtils.removeTab(tab); - } -}); diff --git a/src/zen/tests/pinned/browser_pinned_reset_noswitch.js b/src/zen/tests/pinned/browser_pinned_reset_noswitch.js index 969e68f31d..458e62af96 100644 --- a/src/zen/tests/pinned/browser_pinned_reset_noswitch.js +++ b/src/zen/tests/pinned/browser_pinned_reset_noswitch.js @@ -15,37 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok( - !tab.hasAttribute('zen-pinned-changed'), - 'The tab should not have a zen-pinned-changed attribute after being closed' - ); - ok( - !tab.hasAttribute('discarded'), - 'The tab should not be discarded after being closed' - ); - ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed'); - resolvePromise(); - }, 100); - }, 0); - }, - { once: true } - ); gBrowser.pinTab(tab); + + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok( + !tab.hasAttribute('zen-pinned-changed'), + 'The tab should not have a zen-pinned-changed attribute after being closed' + ); + ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); + ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed'); + resolvePromise(); + }, 100); + }, 0); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_switch.js b/src/zen/tests/pinned/browser_pinned_switch.js index d0a2caa569..8d62e5fc42 100644 --- a/src/zen/tests/pinned/browser_pinned_switch.js +++ b/src/zen/tests/pinned/browser_pinned_switch.js @@ -15,37 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should not have a zen-pinned-changed attribute after being closed' - ); - ok( - !tab.hasAttribute('discarded'), - 'The tab should not be discarded after being closed' - ); - ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); - resolvePromise(); - }, 100); - }, 0); - }, - { once: true } - ); gBrowser.pinTab(tab); + + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should not have a zen-pinned-changed attribute after being closed' + ); + ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); + ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); + resolvePromise(); + }, 100); + }, 0); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_to_essential.js b/src/zen/tests/pinned/browser_pinned_to_essential.js index 51315384dd..2a605c1be1 100644 --- a/src/zen/tests/pinned/browser_pinned_to_essential.js +++ b/src/zen/tests/pinned/browser_pinned_to_essential.js @@ -12,26 +12,18 @@ add_task(async function test_Pinned_To_Essential() { await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true); const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - gZenPinnedTabManager.addToEssentials(newTab); - ok( - newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0', - 'New tab should be marked as essential.' - ); - - resolvePromise(); - }, - { once: true } - ); gBrowser.pinTab(newTab); + ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); + + gZenPinnedTabManager.addToEssentials(newTab); + ok( + newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0', + 'New tab should be marked as essential.' + ); + + resolvePromise(); + await promise; await BrowserTestUtils.removeTab(newTab); }); diff --git a/src/zen/tests/pinned/browser_pinned_unload_changed.js b/src/zen/tests/pinned/browser_pinned_unload_changed.js index 5d7bc6f539..ca4d77cf4f 100644 --- a/src/zen/tests/pinned/browser_pinned_unload_changed.js +++ b/src/zen/tests/pinned/browser_pinned_unload_changed.js @@ -15,35 +15,27 @@ add_task(async function test_Unload_Changed_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); + gBrowser.pinTab(tab); - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok( - !tab.hasAttribute('zen-pinned-changed'), - 'The tab should not have a zen-pinned-changed attribute after being closed' - ); + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok( + !tab.hasAttribute('zen-pinned-changed'), + 'The tab should not have a zen-pinned-changed attribute after being closed' + ); - ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); - ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); - resolvePromise(); - }, 100); - }, 0); - }, - { once: true } - ); - gBrowser.pinTab(tab); + ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); + ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); + resolvePromise(); + }, 100); + }, 0); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_pinned_unload_noreset.js b/src/zen/tests/pinned/browser_pinned_unload_noreset.js index 538c84bac5..0163a5a73f 100644 --- a/src/zen/tests/pinned/browser_pinned_unload_noreset.js +++ b/src/zen/tests/pinned/browser_pinned_unload_noreset.js @@ -15,34 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() { await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => { const tab = gBrowser.getTabForBrowser(browser); - tab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - const pinTabID = tab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); - await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should have a zen-pinned-changed attribute after being pinned' - ); - document.getElementById('cmd_close').doCommand(); - setTimeout(() => { - ok( - tab.hasAttribute('zen-pinned-changed'), - 'The tab should not have a zen-pinned-changed attribute after being closed' - ); - ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); - ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); - resolvePromise(); - }, 100); - }, 0); - }, - { once: true } - ); gBrowser.pinTab(tab); + + BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2'); + await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2'); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should have a zen-pinned-changed attribute after being pinned' + ); + document.getElementById('cmd_close').doCommand(); + setTimeout(() => { + ok( + tab.hasAttribute('zen-pinned-changed'), + 'The tab should not have a zen-pinned-changed attribute after being closed' + ); + ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed'); + ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed'); + resolvePromise(); + }, 100); + }, 0); await promise; }); }); diff --git a/src/zen/tests/pinned/browser_private_mode_no_essentials.js b/src/zen/tests/pinned/browser_private_mode_no_essentials.js index 06b178289c..8de324771f 100644 --- a/src/zen/tests/pinned/browser_private_mode_no_essentials.js +++ b/src/zen/tests/pinned/browser_private_mode_no_essentials.js @@ -11,36 +11,29 @@ add_task(async function test_Private_Mode_No_Essentials() { }); const newTab = gBrowser.selectedTab; - newTab.addEventListener( - 'ZenPinnedTabCreated', - async function (event) { - ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); - - const pinTabID = newTab.getAttribute('zen-pin-id'); - ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned'); - - try { - const pins = await ZenPinnedTabsStorage.getPins(); - const pinObject = pins.find((pin) => pin.uuid === pinTabID); - ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage'); - - let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ - private: true, - }); - await privateWindow.gZenWorkspaces.promiseInitialized; - ok( - !privateWindow.gBrowser.tabs.some((tab) => tab.pinned), - 'Private window should not have any pinned tabs initially' - ); - - await BrowserTestUtils.closeWindow(privateWindow); - } catch (error) { - ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error); - } - resolvePromise(); - }, - { once: true } - ); + gBrowser.pinTab(newTab); + + ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()'); + + try { + const pins = await ZenPinnedTabsStorage.getPins(); + const pinObject = pins.find((pin) => pin.uuid === pinTabID); + ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage'); + + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await privateWindow.gZenWorkspaces.promiseInitialized; + ok( + !privateWindow.gBrowser.tabs.some((tab) => tab.pinned), + 'Private window should not have any pinned tabs initially' + ); + + await BrowserTestUtils.closeWindow(privateWindow); + } catch (error) { + ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error); + } + resolvePromise(); gZenPinnedTabManager.addToEssentials(newTab); await promise; diff --git a/src/zen/tests/welcome/browser_welcome.js b/src/zen/tests/welcome/browser_welcome.js index d780ce9f13..3b95fab201 100644 --- a/src/zen/tests/welcome/browser_welcome.js +++ b/src/zen/tests/welcome/browser_welcome.js @@ -162,7 +162,6 @@ add_task(async function test_Welcome_Steps() { ); Assert.equal(tab.group, group, 'Pinned tabs should belong to the first tab group'); } - ok(tab.hasAttribute('zen-pin-id'), 'Pinned tabs should have a zen-pin-id attribute'); } } group.delete(); diff --git a/src/zen/tests/workspaces/browser_basic_workspaces.js b/src/zen/tests/workspaces/browser_basic_workspaces.js index 2a6ac2017e..dc0bc77675 100644 --- a/src/zen/tests/workspaces/browser_basic_workspaces.js +++ b/src/zen/tests/workspaces/browser_basic_workspaces.js @@ -9,9 +9,9 @@ add_task(async function test_Check_Creation() { const currentWorkspaceUUID = gZenWorkspaces.activeWorkspace; await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2'); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); ok( - currentWorkspaceUUID !== workspaces.workspaces[1].uuid, + currentWorkspaceUUID !== workspaces[1].uuid, 'The new workspace should be different from the current one.' ); @@ -26,7 +26,7 @@ add_task(async function test_Check_Creation() { const workspacesAfterRemove = await gZenWorkspaces._workspaces(); ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.'); ok( - workspacesAfterRemove.workspaces[0].uuid === currentWorkspaceUUID, + workspacesAfterRemove[0].uuid === currentWorkspaceUUID, 'The workspace should be the one we started with.' ); ok(gBrowser.tabs.length === 2, 'There should be one tab.'); diff --git a/src/zen/tests/workspaces/browser_change_to_empty.js b/src/zen/tests/workspaces/browser_change_to_empty.js index c7f6f90b74..9913a988f0 100644 --- a/src/zen/tests/workspaces/browser_change_to_empty.js +++ b/src/zen/tests/workspaces/browser_change_to_empty.js @@ -19,6 +19,6 @@ add_task(async function test_Change_To_Empty() { ); const workspacesAfterRemove = await gZenWorkspaces._workspaces(); - ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.'); + ok(workspacesAfterRemove.length === 1, 'One workspace should exist.'); ok(gBrowser.tabs.length === 2, 'There should be two tabs.'); }); diff --git a/src/zen/tests/workspaces/browser_workspace_bookmarks.js b/src/zen/tests/workspaces/browser_workspace_bookmarks.js index 02734f5dd1..4a9e7ec489 100644 --- a/src/zen/tests/workspaces/browser_workspace_bookmarks.js +++ b/src/zen/tests/workspaces/browser_workspace_bookmarks.js @@ -107,9 +107,9 @@ add_task(async function test_workspace_bookmark() { await withBookmarksShowing(async () => { await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2'); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); - const firstWorkspace = workspaces.workspaces[0]; - const secondWorkspace = workspaces.workspaces[1]; + ok(workspaces.length === 2, 'Two workspaces should exist.'); + const firstWorkspace = workspaces[0]; + const secondWorkspace = workspaces[1]; ok( firstWorkspace.uuid !== secondWorkspace.uuid, 'The new workspace should be different from the current one.' diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 12d2883469..bc161200a9 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -76,7 +76,7 @@ const globalActionsTemplate = [ command: 'cmd_zenWorkspaceForward', icon: 'chrome://browser/skin/zen-icons/forward.svg', isAvailable: (window) => { - return window.gZenWorkspaces._workspaceCache.workspaces.length > 1; + return window.gZenWorkspaces._workspaceCache.length > 1; }, }, { @@ -85,7 +85,7 @@ const globalActionsTemplate = [ icon: 'chrome://browser/skin/zen-icons/back.svg', isAvailable: (window) => { // This also covers the case of being in private mode - return window.gZenWorkspaces._workspaceCache.workspaces.length > 1; + return window.gZenWorkspaces._workspaceCache.length > 1; }, }, { diff --git a/src/zen/welcome/ZenWelcome.mjs b/src/zen/welcome/ZenWelcome.mjs index 3e1074bf54..36c680ef26 100644 --- a/src/zen/welcome/ZenWelcome.mjs +++ b/src/zen/welcome/ZenWelcome.mjs @@ -292,9 +292,6 @@ for (const tab of _tabsToPin) { tab.setAttribute('zen-workspace-id', gZenWorkspaces.activeWorkspace); gBrowser.pinTab(tab); - await new Promise((resolve) => { - tab.addEventListener('ZenPinnedTabCreated', resolve, { once: true }); - }); } for (const tab of _tabsToPinEssentials) { tab.removeAttribute('pending'); // Make it appear loaded diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs index 5ece76491e..a1495d4edc 100644 --- a/src/zen/workspaces/ZenGradientGenerator.mjs +++ b/src/zen/workspaces/ZenGradientGenerator.mjs @@ -1318,7 +1318,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { // Use theme from workspace object or passed theme let workspaceTheme = theme || workspace.theme; - await this.foreachWindowAsActive(async (browser) => { + await this.forEachWindow(async (browser) => { if (!browser.gZenThemePicker?.promiseInitialized) { return; } @@ -1332,7 +1332,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { } // Do not rebuild if the workspace is not the same as the current one - const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace(); + const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace(); if (windowWorkspace.uuid !== uuid) { return; } @@ -1630,13 +1630,12 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { }; }); const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture); - let currentWorkspace = await gZenWorkspaces.getActiveWorkspace(); + let currentWorkspace = gZenWorkspaces.getActiveWorkspace(); if (!skipSave) { - await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient); - await gZenWorkspaces._propagateWorkspaceData(); + currentWorkspace.theme = gradient; + gZenWorkspaces.saveWorkspace(currentWorkspace); gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message'); - currentWorkspace = await gZenWorkspaces.getActiveWorkspace(); } await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null); @@ -1691,7 +1690,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { invalidateGradientCache() { this.#gradientsCache = {}; - window.dispatchEvent(new Event('ZenGradientCacheChanged')); + window.dispatchEvent(new Event('ZenGradientCacheChanged', { bubbles: true })); } getGradientForWorkspace(workspace) { diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs index ce6baa122b..7cb1af16db 100644 --- a/src/zen/workspaces/ZenWorkspace.mjs +++ b/src/zen/workspaces/ZenWorkspace.mjs @@ -37,6 +37,14 @@ class nsZenWorkspace extends MozXULElement { `; } + static get moveTabToButtonMarkup() { + return ` + + `; + } + static get inheritedAttributes() { return { '.zen-workspace-tabs-section': 'zen-workspace-id=id', @@ -88,6 +96,20 @@ class nsZenWorkspace extends MozXULElement { gZenWorkspaces.changeWorkspaceIcon(); }); + if (!gZenWorkspaces.currentWindowIsSyncing) { + let actionsButton = this.indicator.querySelector('.zen-workspaces-actions'); + const moveTabToFragment = window.MozXULElement.parseXULToFragment( + nsZenWorkspace.moveTabToButtonMarkup + ); + actionsButton.after(moveTabToFragment); + actionsButton.setAttribute('hidden', 'true'); + actionsButton = actionsButton.nextElementSibling; + actionsButton.addEventListener('command', (event) => { + event.stopPropagation(); + this.#openMoveTabsToWorkspacePanel(event.target); + }); + } + this.scrollbox._getScrollableElements = () => { const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children]; if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) { @@ -209,7 +231,7 @@ class nsZenWorkspace extends MozXULElement { if (newName === '') { return; } - let workspaces = (await gZenWorkspaces._workspaces()).workspaces; + let workspaces = gZenWorkspaces.getWorkspaces(); let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid); workspaceData.name = newName; await gZenWorkspaces.saveWorkspace(workspaceData); @@ -256,6 +278,36 @@ class nsZenWorkspace extends MozXULElement { this.style.removeProperty('--toolbox-textcolor'); this.style.removeProperty('--zen-primary-color'); } + + #openMoveTabsToWorkspacePanel(button) { + button = button.closest('toolbarbutton'); + if (!button) return; + + const popup = document.getElementById('zenMoveTabsToSyncedWorkspacePopup'); + popup.innerHTML = ''; + + const workspaces = gZenWorkspaces.getWorkspaces(true); + for (const workspace of workspaces) { + const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); + item.addEventListener('command', async () => { + const { ZenWindowSync } = ChromeUtils.importESModule( + 'resource:///modules/zen/ZenWindowSync.sys.mjs' + ); + ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid); + }); + popup.appendChild(item); + } + + button.setAttribute('open', 'true'); + popup.addEventListener( + 'popuphidden', + () => { + button.removeAttribute('open'); + }, + { once: true } + ); + popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */); + } } customElements.define('zen-workspace', nsZenWorkspace); diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs index cbaeed4c2b..d037bade7a 100644 --- a/src/zen/workspaces/ZenWorkspaceCreation.mjs +++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs @@ -199,7 +199,7 @@ class nsZenWorkspaceCreation extends MozXULElement { } async onCreateButtonCommand() { - const workspace = await gZenWorkspaces.getActiveWorkspace(); + const workspace = gZenWorkspaces.getActiveWorkspace(); workspace.name = this.inputName.value.trim(); workspace.icon = this.inputIcon.image || this.inputIcon.label || undefined; workspace.containerTabId = this.currentProfile; @@ -320,8 +320,8 @@ class nsZenWorkspaceCreation extends MozXULElement { this.remove(); gZenUIManager.updateTabsToolbar(); - const workspace = await gZenWorkspaces.getActiveWorkspace(); - await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true); + const workspace = gZenWorkspaces.getActiveWorkspace(); + await gZenWorkspaces._organizeWorkspaceStripLocations(workspace); await gZenWorkspaces.updateTabsContainers(); await gZenUIManager.motion.animate( diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs index f9186c43f2..9672c4a273 100644 --- a/src/zen/workspaces/ZenWorkspaceIcons.mjs +++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs @@ -126,13 +126,13 @@ class nsZenWorkspaceIcons extends MozXULElement { } async #updateIcons() { - const workspaces = await gZenWorkspaces._workspaces(); + const workspaces = gZenWorkspaces.getWorkspaces(); this.innerHTML = ''; - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const button = this.#createWorkspaceIcon(workspace); this.appendChild(button); } - if (workspaces.workspaces.length <= 1) { + if (workspaces.length <= 1) { this.setAttribute('dont-show', 'true'); } else { this.removeAttribute('dont-show'); @@ -168,6 +168,9 @@ class nsZenWorkspaceIcons extends MozXULElement { } i++; } + if (selected == -1) { + return; + } buttons[selected].setAttribute('active', true); this.scrollLeft = buttons[selected].offsetLeft - 10; this.setAttribute('selected', selected); diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 5d87a17c49..9b3941cc95 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -2,18 +2,20 @@ // 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/. -import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs'; import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs'; -class nsZenWorkspaces extends nsZenMultiWindowFeature { +class nsZenWorkspaces { /** * Stores workspace IDs and their last selected tabs. */ - _lastSelectedWorkspaceTabs = {}; - _inChangingWorkspace = false; + lastSelectedWorkspaceTabs = {}; + #inChangingWorkspace = false; draggedElement = null; + #hasInitialized = false; + #canDebug = Services.prefs.getBoolPref('zen.workspaces.debug', false); + #activeWorkspace = ''; _swipeState = { isGestureActive: true, @@ -21,7 +23,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { direction: null, }; - _lastScrollTime = 0; + _workspaceCache = []; + + #lastScrollTime = 0; bookmarkMenus = [ 'PlacesToolbar', @@ -31,18 +35,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { 'BMB_mobileBookmarks', ]; - promiseDBInitialized = new Promise((resolve) => { - this._resolveDBInitialized = resolve; - }); - promisePinnedInitialized = new Promise((resolve) => { this._resolvePinnedInitialized = resolve; }); - promiseSectionsInitialized = new Promise((resolve) => { - this._resolveSectionsInitialized = resolve; - }); - promiseInitialized = new Promise((resolve) => { this._resolveInitialized = resolve; }); @@ -51,11 +47,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (this.privateWindowOrDisabled) { return; } - await Promise.all([ - this.promiseDBInitialized, - this.promisePinnedInitialized, - SessionStore.promiseAllWindowsRestored, - ]); + await Promise.all([this.promisePinnedInitialized, SessionStore.promiseAllWindowsRestored]); } async init() { @@ -116,7 +108,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ChromeUtils.defineLazyGetter(this, 'workspaceIcons', () => document.getElementById('zen-workspaces-button') ); - this._activeWorkspace = Services.prefs.getStringPref('zen.workspaces.active', ''); + this.#activeWorkspace ||= Services.prefs.getStringPref('zen.workspaces.active', ''); if (this.isPrivateWindow) { document.documentElement.setAttribute('zen-private-window', 'true'); @@ -128,8 +120,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.addPopupListeners(); await this.#waitForPromises(); - await this._workspaces(); - await this.afterLoadInit(); } @@ -144,8 +134,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { await this.delayedStartup(); } this._initializeWorkspaceTabContextMenus(); - await this.initializeWorkspaces(); - await this.promiseSectionsInitialized; // Non UI related initializations if ( @@ -156,24 +144,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.initializeGestureHandlers(); this.initializeWorkspaceNavigation(); } - - if (!this.privateWindowOrDisabled) { - const observerFunction = async () => { - this._workspaceBookmarksCache = null; - await this.workspaceBookmarks(); - this._invalidateBookmarkContainers(); - }; - Services.obs.addObserver(this, 'weave:engine:sync:finish'); - Services.obs.addObserver(observerFunction, 'workspace-bookmarks-updated'); - window.addEventListener( - 'unload', - () => { - Services.obs.removeObserver(this, 'weave:engine:sync:finish'); - Services.obs.removeObserver(observerFunction, 'workspace-bookmarks-updated'); - }, - { once: true } - ); - } } // Validate browser state before tab operations @@ -314,18 +284,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }); }; this._pinnedTabsResizeObserver = new ResizeObserver(onResize); - await this._createDefaultWorkspaceIfNeeded(); - } - - async _createDefaultWorkspaceIfNeeded() { - const workspaces = await this._workspaces(); - if (!workspaces.workspaces.length) { - await this.createAndSaveWorkspace('Space', null, true); - this._workspaceCache = null; + if (this.privateWindowOrDisabled) { + await this.restoreWorkspacesFromSessionStore({}); } } - _initializeEmptyTab() { + #initializeEmptyTab() { for (const tab of gBrowser.tabs) { // Check if session store has an empty tab if (tab.hasAttribute('zen-empty-tab') && !tab.pinned) { @@ -405,49 +369,43 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return document.getElementById(workspaceId); } - async initializeTabsStripSections() { + async #initializeTabsStripSections() { await SessionStore.promiseInitialized; await SessionStore.promiseAllWindowsRestored; const perifery = document.getElementById('tabbrowser-arrowscrollbox-periphery'); perifery.setAttribute('hidden', 'true'); - await new Promise((resolve) => { - setTimeout(async () => { - const tabs = gBrowser.tabContainer.allTabs; - const workspaces = await this._workspaces(); - for (const workspace of workspaces.workspaces) { - await this._createWorkspaceTabsSection(workspace, tabs); - } - if (tabs.length) { - const defaultSelectedContainer = this.workspaceElement( - this.activeWorkspace - )?.querySelector('.zen-workspace-normal-tabs-section'); - const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector( - '.zen-workspace-pinned-tabs-section' - ); - // New profile with no workspaces does not have a default selected container - if (defaultSelectedContainer) { - for (const tab of tabs) { - if (tab.hasAttribute('zen-essential')) { - this.getEssentialsSection(tab).appendChild(tab); - continue; - } else if (tab.pinned) { - pinnedContainer.insertBefore(tab, pinnedContainer.lastChild); - continue; - } - // before to the last child (perifery) - defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild); - } + const tabs = gBrowser.tabContainer.allTabs; + const workspaces = this.getWorkspaces(); + for (const workspace of workspaces) { + await this.#createWorkspaceTabsSection(workspace, tabs); + } + if (tabs.length) { + const defaultSelectedContainer = this.workspaceElement(this.activeWorkspace)?.querySelector( + '.zen-workspace-normal-tabs-section' + ); + const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector( + '.zen-workspace-pinned-tabs-section' + ); + // New profile with no workspaces does not have a default selected container + if (defaultSelectedContainer) { + for (const tab of tabs) { + if (tab.hasAttribute('zen-essential')) { + this.getEssentialsSection(tab).appendChild(tab); + continue; + } else if (tab.pinned) { + pinnedContainer.insertBefore(tab, pinnedContainer.lastChild); + continue; } - gBrowser.tabContainer._invalidateCachedTabs(); + // before to the last child (perifery) + defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild); } - perifery.setAttribute('hidden', 'true'); - this._hasInitializedTabsStrip = true; - this.registerPinnedResizeObserver(); - this._fixIndicatorsNames(workspaces); - this._resolveSectionsInitialized(); - resolve(); - }, 0); - }); + } + gBrowser.tabContainer._invalidateCachedTabs(); + } + perifery.setAttribute('hidden', 'true'); + this._hasInitializedTabsStrip = true; + this.registerPinnedResizeObserver(); + this._fixIndicatorsNames(workspaces); } getEssentialsSection(container = 0) { @@ -485,7 +443,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return this.getEssentialsSection(currentWorkspace?.containerTabId); } - async _createWorkspaceTabsSection(workspace, tabs = []) { + async #createWorkspaceTabsSection(workspace, tabs = []) { const workspaceWrapper = document.createXULElement('zen-workspace'); const container = document.getElementById('tabbrowser-arrowscrollbox'); workspaceWrapper.id = workspace.uuid; @@ -608,7 +566,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { toolbox.addEventListener( 'wheel', - async (event) => { + (event) => { if (this.privateWindowOrDisabled) return; // Only process non-gesture scrolls @@ -634,7 +592,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } const currentTime = Date.now(); - if (currentTime - this._lastScrollTime < scrollCooldown) return; + if (currentTime - this.#lastScrollTime < scrollCooldown) return; //this decides which delta to use const delta = isVerticalScroll ? event.deltaY : event.deltaX; @@ -646,7 +604,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { let direction = this.naturalScroll ? -1 : 1; this.changeWorkspaceShortcut(rawDirection * direction); - this._lastScrollTime = currentTime; + this.#lastScrollTime = currentTime; }, { passive: true } ); @@ -701,7 +659,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } _handleSwipeMayStart(event) { - if (this.privateWindowOrDisabled || this._inChangingWorkspace) return; + if (this.privateWindowOrDisabled || this.#inChangingWorkspace) return; if ( event.target.closest('#zen-sidebar-foot-buttons') || event.target.closest('#urlbar[zen-floating-urlbar="true"]') @@ -786,38 +744,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } get activeWorkspace() { - return this._activeWorkspace; + return this.#activeWorkspace; } set activeWorkspace(value) { - this._activeWorkspace = value; + if (value === this.#activeWorkspace) { + return; + } + const spaces = this.getWorkspaces(); + if (!spaces.some((ws) => ws.uuid === value)) { + value = spaces[0]?.uuid || ''; + } + this.#activeWorkspace = value; if (this.privateWindowOrDisabled) { return; } Services.prefs.setStringPref('zen.workspaces.active', value); } - async observe(subject, topic, data) { - if (topic === 'weave:engine:sync:finish' && data === 'workspaces') { - try { - const lastChangeTimestamp = await ZenWorkspacesStorage.getLastChangeTimestamp(); - - if ( - !this._workspaceCache || - !this._workspaceCache.lastChangeTimestamp || - lastChangeTimestamp > this._workspaceCache.lastChangeTimestamp - ) { - await this._propagateWorkspaceData(); - - const currentWorkspace = await this.getActiveWorkspace(); - await gZenThemePicker.onWorkspaceChange(currentWorkspace); - } - } catch (error) { - console.error('Error updating workspaces after sync:', error); - } - } - } - get shouldHaveWorkspaces() { if (typeof this._shouldHaveWorkspaces === 'undefined') { let chromeFlags = window.docShell.treeOwner @@ -835,8 +779,16 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return PrivateBrowsingUtils.isWindowPrivate(window); } + get currentWindowIsSyncing() { + return ( + !document.documentElement.hasAttribute('zen-unsynced-window') && + window._zenStartupSyncFlag !== 'unsynced' && + !this.isPrivateWindow + ); + } + get privateWindowOrDisabled() { - return this.isPrivateWindow || !this.shouldHaveWorkspaces; + return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing; } get workspaceEnabled() { @@ -854,50 +806,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { getWorkspaceFromId(id) { try { - return this._workspaceCache.workspaces.find((workspace) => workspace.uuid === id); + return this.getWorkspaces().find((workspace) => workspace.uuid === id); } catch { return null; } } - async _workspaces() { - if (this._workspaceCache) { - return this._workspaceCache; + getWorkspaces(lieToMe = false) { + if (lieToMe) { + const { ZenSessionStore } = ChromeUtils.importESModule( + 'resource:///modules/zen/ZenSessionManager.sys.mjs' + ); + return ZenSessionStore.getClonedSpaces(); } - - if (this.isPrivateWindow) { - this._workspaceCache = { - workspaces: this._privateWorkspace ? [this._privateWorkspace] : [], - lastChangeTimestamp: 0, - }; - this._activeWorkspace = this._privateWorkspace?.uuid; + if (!this.currentWindowIsSyncing) { + this._workspaceCache = this._tempWorkspace ? [this._tempWorkspace] : []; + this.#activeWorkspace = this._tempWorkspace?.uuid; return this._workspaceCache; } - - const [workspaces, lastChangeTimestamp] = await Promise.all([ - ZenWorkspacesStorage.getWorkspaces(), - ZenWorkspacesStorage.getLastChangeTimestamp(), - ]); - - this._workspaceCache = { workspaces, lastChangeTimestamp }; - // Get the active workspace ID from preferences - const activeWorkspaceId = this.activeWorkspace; - - if (activeWorkspaceId) { - const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId); - // Set the active workspace ID to the first one if the one with selected id doesn't exist - if (!activeWorkspace) { - this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; - } - } else { - // Set the active workspace ID to the first one if active workspace doesn't exist - this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; - } - // sort by position - this._workspaceCache.workspaces.sort( - (a, b) => (a.position ?? Infinity) - (b.position ?? Infinity) - ); - return this._workspaceCache; } @@ -924,8 +850,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return this._workspaceCache; } + async restoreWorkspacesFromSessionStore(aWinData) { + this._workspaceCache = aWinData.spaces || [ + await this.createAndSaveWorkspace('Space', undefined, true), + ]; + this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid; + await this.initializeWorkspaces(); + this.#hasInitialized = true; + } + async initializeWorkspaces() { - let activeWorkspace = await this.getActiveWorkspace(); + let activeWorkspace = this.getActiveWorkspace(); this.activeWorkspace = activeWorkspace?.uuid; await gZenSessionStore.promiseInitialized; try { @@ -936,15 +871,15 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } catch (e) { console.error('gZenWorkspaces: Error initializing theme picker', e); } + await this.#initializeTabsStripSections(); + this.#initializeEmptyTab(); await this.workspaceBookmarks(); - await this.initializeTabsStripSections(); - this._initializeEmptyTab(); - await gZenPinnedTabManager.refreshPinnedTabs({ init: true }); await this.changeWorkspace(activeWorkspace, { onInit: true }); this.#fixTabPositions(); this.onWindowResize(); this._resolveInitialized(); this.#clearAnyZombieTabs(); // Dont call with await + delete this._resolveInitialized; const tabUpdateListener = this.updateTabsContainers.bind(this); window.addEventListener('TabOpen', tabUpdateListener); @@ -955,8 +890,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { window.addEventListener('TabUnpinned', tabUpdateListener); window.addEventListener('aftercustomization', tabUpdateListener); window.addEventListener('TabSelect', this.onLocationChange.bind(this)); - window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this)); + + this.#updateWorkspacesChangeContextMenu(); } async selectStartPage() { @@ -1112,19 +1048,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { shouldCloseWindow() { return ( - !window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') + !window.toolbar.visible || + Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') || + this.privateWindowOrDisabled ); } async #clearAnyZombieTabs() { const tabs = this.allStoredTabs; - const workspaces = await this._workspaces(); + const workspaces = this.getWorkspaces(); for (let tab of tabs) { const workspaceID = tab.getAttribute('zen-workspace-id'); if ( (workspaceID && !tab.hasAttribute('zen-essential') && - !workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)) || + !workspaces.find((workspace) => workspace.uuid === workspaceID)) || // Also remove empty tabs that are supposed to be from parent folders but // they dont exist anymore (tab.pinned && tab.hasAttribute('zen-empty-tab') && !tab.group) @@ -1206,6 +1144,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); } + generateMenuItemForWorkspace(workspace) { + const item = document.createXULElement('menuitem'); + item.className = 'zen-workspace-context-menu-item'; + item.setAttribute('zen-workspace-id', workspace.uuid); + item.setAttribute('disabled', workspace.uuid === this.activeWorkspace); + let name = workspace.name; + const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); + if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { + name = `${workspace.icon} ${name}`; + } + item.setAttribute('label', name); + if (iconIsSvg) { + item.setAttribute('image', workspace.icon); + item.classList.add('zen-workspace-context-icon'); + } + return item; + } + #contextMenuData = null; updateWorkspaceActionsMenu(event) { if (event.target.id !== 'zenWorkspaceMoreActions') { @@ -1248,21 +1204,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (!this.#contextMenuData.workspaceId) { separator.hidden = false; - for (const workspace of [...this._workspaceCache.workspaces].reverse()) { - const item = document.createXULElement('menuitem'); - item.className = 'zen-workspace-context-menu-item'; - item.setAttribute('zen-workspace-id', workspace.uuid); - item.setAttribute('disabled', workspace.uuid === this.activeWorkspace); - let name = workspace.name; - const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); - if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { - name = `${workspace.icon} ${name}`; - } - item.setAttribute('label', name); - if (iconIsSvg) { - item.setAttribute('image', workspace.icon); - item.classList.add('zen-workspace-context-icon'); - } + for (const workspace of [...this._workspaceCache].reverse()) { + const item = this.generateMenuItemForWorkspace(workspace); item.addEventListener('command', (e) => { this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id')); }); @@ -1295,48 +1238,35 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }); } - async saveWorkspace(workspaceData, preventPropagation = false) { + saveWorkspace(workspaceData) { if (this.privateWindowOrDisabled) { return; } - await ZenWorkspacesStorage.saveWorkspace(workspaceData); - if (!preventPropagation) { - await this._propagateWorkspaceData(); - await this._updateWorkspacesChangeContextMenu(); + const workspacesData = this.getWorkspaces(); + const index = workspacesData.findIndex((ws) => ws.uuid === workspaceData.uuid); + if (index !== -1) { + workspacesData[index] = workspaceData; + } else { + workspacesData.push(workspaceData); } + this.#propagateWorkspaceData(); } - async removeWorkspace(windowID) { - let workspacesData = await this._workspaces(); - await this.changeWorkspace( - workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID) - ); - await this.#deleteAllTabsInWorkspace(windowID); - delete this._lastSelectedWorkspaceTabs[windowID]; - await ZenWorkspacesStorage.removeWorkspace(windowID); + removeWorkspace(windowID) { + let workspacesData = this.getWorkspaces(); // Remove the workspace from the cache - this._workspaceCache.workspaces = this._workspaceCache.workspaces.filter( - (workspace) => workspace.uuid !== windowID - ); - await this._propagateWorkspaceData(); - await this._updateWorkspacesChangeContextMenu(); - this.workspaceElement(windowID)?.remove(); - this.onWindowResize(); - this.registerPinnedResizeObserver(); + workspacesData = workspacesData.filter((workspace) => workspace.uuid !== windowID); + this.#propagateWorkspaceData(workspacesData); } isWorkspaceActive(workspace) { return workspace.uuid === this.activeWorkspace; } - async getActiveWorkspace() { - const workspaces = await this._workspaces(); - return ( - workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ?? - workspaces.workspaces[0] - ); + getActiveWorkspace() { + return this.getActiveWorkspaceFromCache(); } - // Workspaces dialog UI management + workspaceHasIcon(workspace) { return workspace.icon && workspace.icon !== ''; } @@ -1359,65 +1289,75 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); } - async _propagateWorkspaceDataForWindow(browser, { ignoreStrip = false, clearCache = true } = {}) { - if (clearCache) { - browser.gZenWorkspaces._workspaceCache = null; - browser.gZenWorkspaces._workspaceBookmarksCache = null; - } - let workspaces = await browser.gZenWorkspaces._workspaces(); - browser.document - .getElementById('cmd_zenCtxDeleteWorkspace') - .setAttribute('disabled', workspaces.workspaces.length <= 1); - if (clearCache) { - browser.dispatchEvent( - new CustomEvent('ZenWorkspacesUIUpdate', { - bubbles: true, - detail: { activeIndex: browser.gZenWorkspaces.activeWorkspace }, - }) - ); - for (const workspace of workspaces.workspaces) { - // Add workspace elements if they dont exist on other windows - if (!browser.gZenWorkspaces.workspaceElement(workspace.uuid)) { - await browser.gZenWorkspaces._createWorkspaceTabsSection(workspace); - } - } - } - await browser.gZenWorkspaces.workspaceBookmarks(); - if (!ignoreStrip) { - browser.gZenWorkspaces._fixIndicatorsNames(workspaces); + #propagateWorkspaceData(aSpaceData = null) { + if (!this.#hasInitialized || this.privateWindowOrDisabled) { + return; } + window.gZenWindowSync.propagateWorkspacesToAllWindows(aSpaceData ?? this._workspaceCache); } - async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) { - const currentWindowIsPrivate = this.isPrivateWindow; - if (onInit) { - if (currentWindowIsPrivate) return; - return await this._propagateWorkspaceDataForWindow(this.ownerWindow, { - ignoreStrip, - clearCache, - }); - } - await this.foreachWindowAsActive(async (browser) => { - // Do not update the window if workspaces are not enabled in it. - // For example, when the window is in private browsing mode. + propagateWorkspaces(aWorkspaces) { + const previousWorkspaces = this._workspaceCache || []; + this._workspaceCache = aWorkspaces; + let hasChanged = false; + // Remove any workspace elements here that no longer exist + for (const previousWorkspace of previousWorkspaces) { if ( - !browser.gZenWorkspaces.workspaceEnabled || - browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate + this.workspaceElement(previousWorkspace.uuid) && + !aWorkspaces.find((w) => w.uuid === previousWorkspace.uuid) ) { - return; + if (this.isWorkspaceActive(previousWorkspace)) { + // If the removed workspace was active, switch to another one + const newActiveWorkspace = + aWorkspaces.find((w) => w.uuid !== previousWorkspace.uuid) || null; + this.changeWorkspace(newActiveWorkspace); + } + this.workspaceElement(previousWorkspace.uuid)?.remove(); + delete this.lastSelectedWorkspaceTabs[previousWorkspace.uuid]; + hasChanged = true; + } + } + // Add any new workspace elements here + for (const workspace of aWorkspaces) { + if (!this.workspaceElement(workspace.uuid)) { + this.#createWorkspaceTabsSection(workspace).catch((e) => { + console.error('Error creating workspace tabs section:', e); + }); + hasChanged = true; + } + } + // Order the workspace elements correctly + let previousElement = null; + for (const workspace of aWorkspaces) { + const workspaceElement = this.workspaceElement(workspace.uuid); + if (workspaceElement) { + if (previousElement === null) { + gZenUIManager.tabsWrapper.insertBefore( + workspaceElement, + gZenUIManager.tabsWrapper.firstChild + ); + hasChanged = true; + } else if (previousElement.nextSibling !== workspaceElement) { + gZenUIManager.tabsWrapper.insertBefore(workspaceElement, previousElement.nextSibling); + hasChanged = true; + } + previousElement = workspaceElement; } - this._propagateWorkspaceDataForWindow(browser, { - ignoreStrip, - clearCache, - }).catch(console.error); + } + if (hasChanged) { + this.#fireSpaceUIUpdate(); + } + this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache()).finally(() => { + this.updateTabsContainers(); }); + this.#updateWorkspacesChangeContextMenu(); } async reorderWorkspace(id, newPosition) { if (this.privateWindowOrDisabled) { return; } - const workspaces = (await this._workspaces()).workspaces; + const workspaces = this.getWorkspaces(); const workspace = workspaces.find((w) => w.uuid === id); if (!workspace) { console.warn(`Workspace with ID ${id} not found for reordering.`); @@ -1436,26 +1376,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } workspaces.splice(newPosition, 0, workspace); - // Update the positions in the storage - await ZenWorkspacesStorage.updateWorkspacePositions(workspaces); // Propagate the changes - await this._propagateWorkspaceData(); - } - - async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) { - const workspaces = (await this._workspaces()).workspaces; - const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId); - const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0]; - const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId); - workspaces.splice(targetIndex, 0, draggedWorkspace); - - await ZenWorkspacesStorage.updateWorkspacePositions(workspaces); - await this._propagateWorkspaceData(); + this.#propagateWorkspaceData(); } async openWorkspaceCreation() { let createForm; - const previousWorkspace = await this.getActiveWorkspace(); + const previousWorkspace = this.getActiveWorkspace(); document.documentElement.setAttribute('zen-creating-workspace', 'true'); await this.createAndSaveWorkspace('Space', undefined, false, 0, { beforeChangeCallback: async (workspace) => { @@ -1469,27 +1396,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { createForm.finishSetup(); } - // Workspaces management - - async #deleteAllTabsInWorkspace(workspaceID) { - const tabs = Array.from(this.allStoredTabs).filter( - (tab) => - tab.getAttribute('zen-workspace-id') === workspaceID && - !tab.hasAttribute('zen-empty-tab') && - !tab.hasAttribute('zen-essential') - ); - for (const tab of tabs) { - if (tab.pinned) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - } - } - gBrowser.removeTabs(tabs, { - animate: false, - skipSessionStore: true, - closeWindowWithLastTab: false, - }); - } - #unpinnedTabsInWorkspace(workspaceID) { return Array.from(this.allStoredTabs).filter( (tab) => tab.getAttribute('zen-workspace-id') === workspaceID && tab.visible && !tab.pinned @@ -1630,19 +1536,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async changeWorkspace(workspace, ...args) { if ( !this.workspaceEnabled || - this._inChangingWorkspace || + this.#inChangingWorkspace || gNavToolbox.hasAttribute('movingtab') ) { return; } - this._inChangingWorkspace = true; + this.#inChangingWorkspace = true; try { this.log('Changing workspace to', workspace?.uuid); await this._performWorkspaceChange(workspace, ...args); } catch (e) { console.error('gZenWorkspaces: Error changing workspace', e); } - this._inChangingWorkspace = false; + this.#inChangingWorkspace = false; } _cancelSwipeAnimation() { @@ -1653,7 +1559,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { workspace, { onInit = false, alwaysChange = false, whileScrolling = false } = {} ) { - const previousWorkspace = await this.getActiveWorkspace(); + const previousWorkspace = this.getActiveWorkspace(); alwaysChange = alwaysChange || onInit; this.activeWorkspace = workspace.uuid; if (previousWorkspace && previousWorkspace.uuid === workspace.uuid && !alwaysChange) { @@ -1661,11 +1567,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } - const workspaces = await this._workspaces(); + const workspaces = this.getWorkspaces(); gZenFolders.cancelPopupTimer(); // Refresh tab cache - for (const otherWorkspace of workspaces.workspaces) { + for (const otherWorkspace of workspaces) { const container = this.workspaceElement(otherWorkspace.uuid); container.active = otherWorkspace.uuid === workspace.uuid; } @@ -1687,9 +1593,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { gBrowser.warmupTab(tabToSelect); // Update UI and state - const previousWorkspaceIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === previousWorkspace.uuid - ); + const previousWorkspaceIndex = workspaces.findIndex((w) => w.uuid === previousWorkspace.uuid); await this._updateWorkspaceState(workspace, onInit, tabToSelect, { previousWorkspaceIndex, previousWorkspace, @@ -1747,7 +1651,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ) { if ( workspaceElement && - !(this._inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop) + !(this.#inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop) ) { delete this._alwaysAnimatePaddingTop; const essentialsHeight = essentialContainer.getBoundingClientRect().height; @@ -1777,8 +1681,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } this._organizingWorkspaceStrip = true; - const workspaces = await this._workspaces(); - let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid); + const workspaces = this.getWorkspaces(); + let workspaceIndex = workspaces.findIndex((w) => w.uuid === workspace.uuid); if (!justMove) { this._fixIndicatorsNames(workspaces); } @@ -1787,10 +1691,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); const workspaceContextId = workspace.containerTabId; const nextWorkspaceContextId = - workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId; - for (const otherWorkspace of workspaces.workspaces) { + workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId; + for (const otherWorkspace of workspaces) { const element = this.workspaceElement(otherWorkspace.uuid); - const newTransform = -(workspaceIndex - workspaces.workspaces.indexOf(otherWorkspace)) * 100; + const newTransform = -(workspaceIndex - workspaces.indexOf(otherWorkspace)) * 100; element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`; } // Hide other essentials with different containerTabId @@ -1824,7 +1728,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (offsetPixels) { // Find the next workspace we are scrolling to - const nextWorkspace = workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]; + const nextWorkspace = workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]; if (nextWorkspace) { const { gradient: nextGradient, @@ -1860,7 +1764,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const grainValue = minGrain + (maxGrain - minGrain) * (existingGrain > nextGrain ? 1 - percentage : percentage); - if (!this._inChangingWorkspace) { + if (!this.#inChangingWorkspace) { gZenThemePicker.updateNoise(grainValue); } } @@ -1895,7 +1799,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } _fixIndicatorsNames(workspaces) { - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const workspaceIndicator = this.workspaceElement(workspace.uuid)?.indicator; this.updateWorkspaceIndicator(workspace, workspaceIndicator); } @@ -1911,12 +1815,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const kGlobalAnimationDuration = 0.2; this._animatingChange = true; const animations = []; - const workspaces = await this._workspaces(); - const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); + const workspaces = this.getWorkspaces(); + const newWorkspaceIndex = workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex; const clonedEssentials = []; if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) { - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const essentialsContainer = this.getEssentialsSection(workspace.containerTabId); if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) { clonedEssentials[clonedEssentials.length - 1].repeat++; @@ -1984,9 +1888,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } const existingTransform = element.style.transform; const elementWorkspaceId = element.id; - const elementWorkspaceIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === elementWorkspaceId - ); + const elementWorkspaceIndex = workspaces.findIndex((w) => w.uuid === elementWorkspaceId); const offset = -(newWorkspaceIndex - elementWorkspaceIndex) * 100; const newTransform = `translateX(${offset}%)`; if (shouldAnimate) { @@ -2023,10 +1925,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Get a list of essentials containers that are in between the first and last workspace const essentialsContainersInBetween = clonedEssentials.filter((cloned) => { const essentialsWorkspaces = cloned.workspaces; - const firstIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === essentialsWorkspaces[0].uuid - ); - const lastIndex = workspaces.workspaces.findIndex( + const firstIndex = workspaces.findIndex((w) => w.uuid === essentialsWorkspaces[0].uuid); + const lastIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid ); @@ -2055,10 +1955,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // will slide in from the right // Get the index from first and last workspace - const firstWorkspaceIndex = workspaces.workspaces.findIndex( + const firstWorkspaceIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[0].uuid ); - const lastWorkspaceIndex = workspaces.workspaces.findIndex( + const lastWorkspaceIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid ); cloned.originalContainer.style.removeProperty('transform'); @@ -2214,7 +2114,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { tab, currentWorkspace.uuid, currentWorkspace.containerTabId, - await this._workspaces() + this.getWorkspaces() ); } @@ -2249,9 +2149,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return ( !tabContextId || tabContextId === '0' || - !workspaces.workspaces.some( - (workspace) => workspace.containerTabId === parseInt(tabContextId, 10) - ) + !workspaces.some((workspace) => workspace.containerTabId === parseInt(tabContextId, 10)) ); } } @@ -2270,14 +2168,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async _handleTabSelection(workspace, onInit, previousWorkspaceId) { const currentSelectedTab = gBrowser.selectedTab; const oldWorkspaceId = previousWorkspaceId; - const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid]; + const lastSelectedTab = this.lastSelectedWorkspaceTabs[workspace.uuid]; const containerId = workspace.containerTabId?.toString(); - const workspaces = await this._workspaces(); + const workspaces = this.getWorkspaces(); // Save current tab as last selected for old workspace if it shouldn't be visible in new workspace if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) { - this._lastSelectedWorkspaceTabs[oldWorkspaceId] = + this.lastSelectedWorkspaceTabs[oldWorkspaceId] = gZenGlanceManager.getTabOrGlanceParent(currentSelectedTab); } @@ -2333,9 +2231,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { gBrowser.tabContainer.arrowScrollbox = this.activeScrollbox; // Update workspace UI - await this._updateWorkspacesChangeContextMenu(); - await this._propagateWorkspaceData({ clearCache: false, onInit }); - gZenThemePicker.onWorkspaceChange(workspace); gZenUIManager.tabsWrapper.scrollbarWidth = 'none'; @@ -2380,15 +2275,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { tab.setAttribute('zen-workspace-id', workspace.uuid); } } - window.dispatchEvent( - new CustomEvent('ZenWorkspacesUIUpdate', { - bubbles: true, - detail: { activeIndex: workspace.uuid }, - }) - ); + this.#fireSpaceUIUpdate(); } } + #fireSpaceUIUpdate() { + window.dispatchEvent( + new CustomEvent('ZenWorkspacesUIUpdate', { + bubbles: true, + detail: { activeIndex: this.activeWorkspace }, + }) + ); + } + async _fixCtrlTabBehavior() { ctrlTab.uninit(); ctrlTab.readPref(); @@ -2404,9 +2303,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } } - async _updateWorkspacesChangeContextMenu() { + #updateWorkspacesChangeContextMenu() { if (gZenWorkspaces.privateWindowOrDisabled) return; - const workspaces = await this._workspaces(); + const workspaces = this.getWorkspaces(); const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup'); if (!menuPopup) { @@ -2414,9 +2313,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } menuPopup.innerHTML = ''; - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); - for (let workspace of workspaces.workspaces) { + for (let workspace of workspaces) { const menuItem = document.createXULElement('menuitem'); menuItem.setAttribute('label', workspace.name); menuItem.setAttribute('zen-workspace-id', workspace.uuid); @@ -2440,7 +2339,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }; if (moveTabs) { this.#prepareNewWorkspace(workspace); - await this._createWorkspaceTabsSection(workspace, tabs); + await this.#createWorkspaceTabsSection(workspace, tabs); await this._organizeWorkspaceStripLocations(workspace); } return workspace; @@ -2456,8 +2355,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (!this.workspaceEnabled) { return; } - if (this.isPrivateWindow) { - name = 'Private ' + name; + if (!this.currentWindowIsSyncing) { + name = this.isPrivateWindow ? 'Private ' + name : 'Temporary'; } // get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter( @@ -2474,10 +2373,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { !dontChange, containerTabId ); - if (this.isPrivateWindow) { - this._privateWorkspace = workspaceData; + if (!this.currentWindowIsSyncing) { + this._tempWorkspace = workspaceData; } else { - await this.saveWorkspace(workspaceData, dontChange); + this.saveWorkspace(workspaceData); } if (!dontChange) { if (beforeChangeCallback) { @@ -2582,9 +2481,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const workspacesIds = []; if (entry.target.closest('#zen-essentials')) { // Get all workspaces that have the same userContextId - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); const userContextId = activeWorkspace.containerTabId; - const workspaces = this._workspaceCache.workspaces.filter( + const workspaces = this._workspaceCache.filter( (w) => w.containerTabId === userContextId && w.uuid !== originalWorkspaceId ); workspacesIds.push(...workspaces.map((w) => w.uuid)); @@ -2637,7 +2536,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (workspaceID) { if (tab.hasAttribute('change-workspace') && this.moveTabToWorkspace(tab, workspaceID)) { - this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab); + this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab); tab.removeAttribute('change-workspace'); const workspace = this.getWorkspaceFromId(workspaceID); setTimeout(() => { @@ -2647,7 +2546,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } - let activeWorkspace = await this.getActiveWorkspace(); + let activeWorkspace = this.getActiveWorkspace(); if (!activeWorkspace) { return; } @@ -2669,7 +2568,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async onLocationChange(event) { let tab = event.target; this.#changeToEmptyTab(); - if (!this.workspaceEnabled || this._inChangingWorkspace || this._isClosingWindow) { + if (!this.workspaceEnabled || this.#inChangingWorkspace || this._isClosingWindow) { return; } @@ -2686,14 +2585,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (!isEssential) { - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); if (!activeWorkspace) { return; } // Only update last selected tab for non-essential tabs in their workspace if (workspaceID === activeWorkspace.uuid) { - this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab); + this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab); } // Switch workspace if needed @@ -2710,16 +2609,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Context menu management async contextChangeContainerTab(event) { this._organizingWorkspaceStrip = true; - let workspaces = await this._workspaces(); - let workspace = workspaces.workspaces.find( + let workspaces = this.getWorkspaces(); + let workspace = workspaces.find( (workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace) ); let userContextId = parseInt(event.target.getAttribute('data-usercontextid')); workspace.containerTabId = userContextId + 0; // +0 to convert to number - await this.saveWorkspace(workspace); - await this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache(), true); - await this.updateTabsContainers(); - this.tabContainer._invalidateCachedTabs(); + this.saveWorkspace(workspace); } async closeAllUnpinnedTabs() { @@ -2751,7 +2647,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }, ]); if (Services.prompt.confirm(null, title, body)) { - await this.removeWorkspace(workspaceId); + this.removeWorkspace(workspaceId); } } @@ -2764,21 +2660,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async changeWorkspaceShortcut(offset = 1, whileScrolling = false) { // Cycle through workspaces - let workspaces = await this._workspaces(); - let activeWorkspace = await this.getActiveWorkspace(); - let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace); + let workspaces = this.getWorkspaces(); + let activeWorkspace = this.getActiveWorkspace(); + let workspaceIndex = workspaces.indexOf(activeWorkspace); // note: offset can be negative let targetIndex = workspaceIndex + offset; if (this.shouldWrapAroundNavigation) { // Add length to handle negative indices and loop - targetIndex = (targetIndex + workspaces.workspaces.length) % workspaces.workspaces.length; + targetIndex = (targetIndex + workspaces.length) % workspaces.length; } else { // Clamp within bounds to disable looping - targetIndex = Math.max(0, Math.min(workspaces.workspaces.length - 1, targetIndex)); + targetIndex = Math.max(0, Math.min(workspaces.length - 1, targetIndex)); } - let nextWorkspace = workspaces.workspaces[targetIndex]; + let nextWorkspace = workspaces[targetIndex]; await this.changeWorkspace(nextWorkspace, { whileScrolling }); } @@ -2818,20 +2714,18 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const previousWorkspaceID = document.documentElement.getAttribute('zen-workspace-id'); for (let tab of tabs) { this.moveTabToWorkspace(tab, workspaceID); - if (this._lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) { + if (this.lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) { // This tab is no longer the last selected tab in the previous workspace because it's being moved to // the current workspace - delete this._lastSelectedWorkspaceTabs[previousWorkspaceID]; + delete this.lastSelectedWorkspaceTabs[previousWorkspaceID]; } } // Make sure we select the last tab in the new workspace - this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent( + this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent( tabs[tabs.length - 1] ); - const workspaces = await this._workspaces(); - await this.changeWorkspace( - workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID) - ); + const workspaces = this.getWorkspaces(); + await this.changeWorkspace(workspaces.find((workspace) => workspace.uuid === workspaceID)); } // Tab browser utilities @@ -2844,11 +2738,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if ( this.shouldForceContainerTabsToWorkspace && typeof userContextId !== 'undefined' && - this._workspaceCache?.workspaces && + this._workspaceCache && !fromExternal ) { // Find all workspaces that match the given userContextId - const matchingWorkspaces = this._workspaceCache.workspaces.filter( + const matchingWorkspaces = this._workspaceCache.filter( (workspace) => workspace.containerTabId === userContextId ); @@ -2887,12 +2781,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async shortcutSwitchTo(index) { - const workspaces = await this._workspaces(); + const workspaces = this.getWorkspaces(); // The index may be out of bounds, if it doesnt exist, don't do anything - if (index >= workspaces.workspaces.length || index < 0) { + if (index >= workspaces.length || index < 0) { return; } - const workspaceToSwitch = workspaces.workspaces[index]; + const workspaceToSwitch = workspaces[index]; await this.changeWorkspace(workspaceToSwitch); } @@ -2936,7 +2830,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { pinnedContainers = [document.getElementById('pinned-tabs-container')]; normalContainers = [this.activeWorkspaceStrip]; } else { - let workspaces = Array.from(this._workspaceCache?.workspaces || []); + let workspaces = Array.from(this._workspaceCache || []); // Make the active workspace first workspaces = workspaces.sort((a, b) => a.uuid === this.activeWorkspace ? -1 : b.uuid === this.activeWorkspace ? 1 : 0 @@ -2982,7 +2876,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } const pinnedContainers = []; const normalContainers = []; - for (const workspace of this._workspaceCache.workspaces) { + for (const workspace of this._workspaceCache) { const container = this.workspaceElement(workspace.uuid); if (container) { pinnedContainers.push(container.pinnedTabsContainer); @@ -3080,7 +2974,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Find first workspace with the same container const containerTabId = parseInt(tab.parentNode.getAttribute('container')); // +0 to convert to number - workspaceToSwitch = this._workspaceCache.workspaces.find( + workspaceToSwitch = this._workspaceCache.find( (workspace) => workspace.containerTabId + 0 === containerTabId ); } else { @@ -3094,7 +2988,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this._workspaceChangeInProgress = true; try { - this._lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] = + this.lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] = gZenGlanceManager.getTabOrGlanceParent(tab); await this.changeWorkspace(workspaceToSwitch); } finally { @@ -3118,7 +3012,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return 0; } const activeWorkspace = this.activeWorkspace; - const workspace = workspaces.workspaces.find((workspace) => workspace.uuid === activeWorkspace); + const workspace = workspaces.find((workspace) => workspace.uuid === activeWorkspace); return workspace.containerTabId; } diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs index 43d183d39f..63097e2405 100644 --- a/src/zen/workspaces/ZenWorkspacesStorage.mjs +++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs @@ -2,437 +2,20 @@ // 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/. -window.ZenWorkspacesStorage = { +// Integration of workspace-specific bookmarks into Places +window.ZenWorkspaceBookmarksStorage = { lazy: {}, async init() { ChromeUtils.defineESModuleGetters(this.lazy, { PlacesUtils: 'resource://gre/modules/PlacesUtils.sys.mjs', - Weave: 'resource://services-sync/main.sys.mjs', }); - if (!window.gZenWorkspaces) return; await this._ensureTable(); - await ZenWorkspaceBookmarksStorage.init(); }, async _ensureTable() { await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage._ensureTable', - async (db) => { - // Create the main workspaces table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_workspaces ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - name TEXT NOT NULL, - icon TEXT, - container_id INTEGER, - position INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Add new columns if they don't exist - // SQLite doesn't have a direct "ADD COLUMN IF NOT EXISTS" syntax, - // so we need to check if the columns exist first - const columns = await db.execute(`PRAGMA table_info(zen_workspaces)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_workspaces ADD COLUMN ${columnName} ${definition}`); - } - }; - - // Add each new column if it doesn't exist - await addColumnIfNotExists('theme_type', 'TEXT'); - await addColumnIfNotExists('theme_colors', 'TEXT'); - await addColumnIfNotExists('theme_opacity', 'REAL'); - await addColumnIfNotExists('theme_rotation', 'INTEGER'); - await addColumnIfNotExists('theme_texture', 'REAL'); - - // Create an index on the uuid column - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_workspaces_uuid ON zen_workspaces(uuid) - `); - - // Create the changes tracking table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_workspaces_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - // Create an index on the uuid column for changes tracking table - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_workspaces_changes_uuid ON zen_workspaces_changes(uuid) - `); - - if (!this.lazy.Weave.Service.engineManager.get('workspaces')) { - this.lazy.Weave.Service.engineManager.register(ZenWorkspacesEngine); - await ZenWorkspacesStorage.migrateWorkspacesFromJSON(); - } - - gZenWorkspaces._resolveDBInitialized(); - } - ); - }, - - async migrateWorkspacesFromJSON() { - const oldWorkspacesPath = PathUtils.join( - PathUtils.profileDir, - 'zen-workspaces', - 'Workspaces.json' - ); - if (await IOUtils.exists(oldWorkspacesPath)) { - console.info('ZenWorkspacesStorage: Migrating workspaces from JSON...'); - const oldWorkspaces = await IOUtils.readJSON(oldWorkspacesPath); - if (oldWorkspaces.workspaces) { - for (const workspace of oldWorkspaces.workspaces) { - await this.saveWorkspace(workspace); - } - } - await IOUtils.remove(oldWorkspacesPath); - } - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyWorkspacesChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async saveWorkspace(workspace, notifyObservers = true) { - const changedUUIDs = new Set(); - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.saveWorkspace', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in workspace && Number.isFinite(workspace.position)) { - newPosition = workspace.position; - } else { - // Get the maximum position - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_workspaces` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; // Add a large increment to avoid frequent reordering - } - - // Insert or replace the workspace - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_workspaces ( - uuid, name, icon, container_id, created_at, updated_at, "position", - theme_type, theme_colors, theme_opacity, theme_rotation, theme_texture - ) VALUES ( - :uuid, :name, :icon, :container_id, - COALESCE((SELECT created_at FROM zen_workspaces WHERE uuid = :uuid), :now), - :now, - :position, - :theme_type, :theme_colors, :theme_opacity, :theme_rotation, :theme_texture - ) - `, - { - uuid: workspace.uuid, - name: workspace.name, - icon: workspace.icon || null, - container_id: workspace.containerTabId || null, - now, - position: newPosition, - theme_type: workspace.theme?.type || null, - theme_colors: workspace.theme ? JSON.stringify(workspace.theme.gradientColors) : null, - theme_opacity: workspace.theme?.opacity || null, - theme_rotation: workspace.theme?.rotation || null, - theme_texture: workspace.theme?.texture || null, - } - ); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: workspace.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(workspace.uuid); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs)); - } - }, - - async getWorkspaces() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_workspaces ORDER BY created_at ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - name: row.getResultByName('name'), - icon: row.getResultByName('icon'), - containerTabId: row.getResultByName('container_id') ?? 0, - position: row.getResultByName('position'), - theme: row.getResultByName('theme_type') - ? { - type: row.getResultByName('theme_type'), - gradientColors: JSON.parse(row.getResultByName('theme_colors')), - opacity: row.getResultByName('theme_opacity'), - rotation: row.getResultByName('theme_rotation'), - texture: row.getResultByName('theme_texture'), - } - : null, - })); - }, - - async removeWorkspace(uuid, notifyObservers = true) { - const changedUUIDs = [uuid]; - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.removeWorkspace', - async (db) => { - await db.execute( - ` - DELETE FROM zen_workspaces WHERE uuid = :uuid - `, - { uuid } - ); - - // Record the removal as a change - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - ); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-removed', changedUUIDs); - } - }, - - async wipeAllWorkspaces() { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.wipeAllWorkspaces', - async (db) => { - await db.execute(`DELETE FROM zen_workspaces`); - await db.execute(`DELETE FROM zen_workspaces_changes`); - await this.updateLastChangeTimestamp(db); - } - ); - }, - - async markChanged(uuid) { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.markChanged', - async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - ); - }, - - async saveWorkspaceTheme(uuid, theme, notifyObservers = true) { - const changedUUIDs = [uuid]; - await this.lazy.PlacesUtils.withConnectionWrapper('saveWorkspaceTheme', async (db) => { - await db.execute( - ` - UPDATE zen_workspaces - SET - theme_type = :type, - theme_colors = :colors, - theme_opacity = :opacity, - theme_rotation = :rotation, - theme_texture = :texture, - updated_at = :now - WHERE uuid = :uuid - `, - { - type: theme.type, - colors: JSON.stringify(theme.gradientColors), - opacity: theme.opacity, - rotation: theme.rotation, - texture: theme.texture, - now: Date.now(), - uuid, - } - ); - - await this.markChanged(uuid); - await this.updateLastChangeTimestamp(db); - }); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-updated', changedUUIDs); - } - }, - - async getChangedIDs() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_workspaces_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.clearChangedIDs', - async (db) => { - await db.execute(`DELETE FROM zen_workspaces_changes`); - } - ); - }, - - shouldReorderWorkspaces(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllWorkspaces(db, changedUUIDs) { - const workspaces = await db.execute(` - SELECT uuid - FROM zen_workspaces - ORDER BY "position" ASC - `); - - for (let i = 0; i < workspaces.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_workspaces - SET "position" = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: workspaces[i].getResultByName('uuid') } - ); - changedUUIDs.add(workspaces[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_workspaces_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_workspaces_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updateWorkspacePositions(workspaces) { - const changedUUIDs = new Set(); - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.updateWorkspacePositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < workspaces.length; i++) { - const workspace = workspaces[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_workspaces - SET "position" = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: workspace.uuid } - ); - - changedUUIDs.add(workspace.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: workspace.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs)); - }, -}; - -// Integration of workspace-specific bookmarks into Places -window.ZenWorkspaceBookmarksStorage = { - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await ZenWorkspacesStorage.lazy.PlacesUtils.withConnectionWrapper( 'ZenWorkspaceBookmarksStorage.init', async (db) => { // Create table using GUIDs instead of IDs @@ -498,7 +81,7 @@ window.ZenWorkspaceBookmarksStorage = { * @returns {Promise} The timestamp of the last change. */ async getLastChangeTimestamp() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const result = await db.executeCached(` SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change' `); @@ -506,7 +89,7 @@ window.ZenWorkspaceBookmarksStorage = { }, async getBookmarkWorkspaces(bookmarkGuid) { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute( ` @@ -531,7 +114,7 @@ window.ZenWorkspaceBookmarksStorage = { * } */ async getBookmarkGuidsByWorkspace() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute(` SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids @@ -554,7 +137,7 @@ window.ZenWorkspaceBookmarksStorage = { * @returns {Promise} An object mapping bookmark+workspace pairs to their change data. */ async getChangedIDs() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute(` SELECT bookmark_guid, workspace_uuid, change_type, timestamp FROM zen_bookmarks_workspaces_changes @@ -575,7 +158,7 @@ window.ZenWorkspaceBookmarksStorage = { * Clear all recorded changes. */ async clearChangedIDs() { - await ZenWorkspacesStorage.lazy.PlacesUtils.withConnectionWrapper( + await this.lazy.PlacesUtils.withConnectionWrapper( 'ZenWorkspaceBookmarksStorage.clearChangedIDs', async (db) => { await db.execute(`DELETE FROM zen_bookmarks_workspaces_changes`); @@ -584,4 +167,4 @@ window.ZenWorkspaceBookmarksStorage = { }, }; -ZenWorkspacesStorage.init(); +ZenWorkspaceBookmarksStorage.init(); diff --git a/src/zen/workspaces/ZenWorkspacesSync.mjs b/src/zen/workspaces/ZenWorkspacesSync.mjs deleted file mode 100644 index 6f2633eec7..0000000000 --- a/src/zen/workspaces/ZenWorkspacesSync.mjs +++ /dev/null @@ -1,459 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// 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/. - -var { Tracker, Store, SyncEngine } = ChromeUtils.importESModule( - 'resource://services-sync/engines.sys.mjs' -); -var { CryptoWrapper } = ChromeUtils.importESModule('resource://services-sync/record.sys.mjs'); -var { Utils } = ChromeUtils.importESModule('resource://services-sync/util.sys.mjs'); -var { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( - 'resource://services-sync/constants.sys.mjs' -); - -// Define ZenWorkspaceRecord -function ZenWorkspaceRecord(collection, id) { - CryptoWrapper.call(this, collection, id); -} - -ZenWorkspaceRecord.prototype = Object.create(CryptoWrapper.prototype); -ZenWorkspaceRecord.prototype.constructor = ZenWorkspaceRecord; - -ZenWorkspaceRecord.prototype._logName = 'Sync.Record.ZenWorkspace'; - -Utils.deferGetSet(ZenWorkspaceRecord, 'cleartext', [ - 'name', - 'icon', - 'default', - 'containerTabId', - 'position', - 'theme_type', - 'theme_colors', - 'theme_opacity', - 'theme_rotation', - 'theme_texture', -]); - -// Define ZenWorkspacesStore -function ZenWorkspacesStore(name, engine) { - Store.call(this, name, engine); -} - -ZenWorkspacesStore.prototype = Object.create(Store.prototype); -ZenWorkspacesStore.prototype.constructor = ZenWorkspacesStore; - -/** - * Initializes the store by loading the current changeset. - */ -ZenWorkspacesStore.prototype.initialize = async function () { - await Store.prototype.initialize.call(this); - // Additional initialization if needed -}; - -/** - * Retrieves all workspace IDs from the storage. - * @returns {Object} An object mapping workspace UUIDs to true. - */ -ZenWorkspacesStore.prototype.getAllIDs = async function () { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const ids = {}; - for (const workspace of workspaces) { - ids[workspace.uuid] = true; - } - return ids; - } catch (error) { - this._log.error('Error fetching all workspace IDs', error); - throw error; - } -}; - -/** - * Handles changing the ID of a workspace. - * @param {String} oldID - The old UUID. - * @param {String} newID - The new UUID. - */ -ZenWorkspacesStore.prototype.changeItemID = async function (oldID, newID) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const workspace = workspaces.find((ws) => ws.uuid === oldID); - if (workspace) { - workspace.uuid = newID; - await ZenWorkspacesStorage.saveWorkspace(workspace, false); - // Mark the new ID as changed for sync - await ZenWorkspacesStorage.markChanged(newID); - } - } catch (error) { - this._log.error(`Error changing workspace ID from ${oldID} to ${newID}`, error); - throw error; - } -}; - -/** - * Checks if a workspace exists. - * @param {String} id - The UUID of the workspace. - * @returns {Boolean} True if the workspace exists, false otherwise. - */ -ZenWorkspacesStore.prototype.itemExists = async function (id) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - return workspaces.some((ws) => ws.uuid === id); - } catch (error) { - this._log.error(`Error checking if workspace exists with ID ${id}`, error); - throw error; - } -}; - -/** - * Creates a record for a workspace. - * @param {String} id - The UUID of the workspace. - * @param {String} collection - The collection name. - * @returns {ZenWorkspaceRecord} The workspace record. - */ -ZenWorkspacesStore.prototype.createRecord = async function (id, collection) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const workspace = workspaces.find((ws) => ws.uuid === id); - const record = new ZenWorkspaceRecord(collection, id); - - if (workspace) { - record.name = workspace.name; - record.icon = workspace.icon; - record.default = workspace.default; - record.containerTabId = workspace.containerTabId; - record.position = workspace.position; - if (workspace.theme) { - record.theme_type = workspace.theme.type; - record.theme_colors = JSON.stringify(workspace.theme.gradientColors); - record.theme_opacity = workspace.theme.opacity; - record.theme_rotation = workspace.theme.rotation; - record.theme_texture = workspace.theme.texture; - } - record.deleted = false; - } else { - record.deleted = true; - } - - return record; - } catch (error) { - this._log.error(`Error creating record for workspace ID ${id}`, error); - throw error; - } -}; - -/** - * Creates a new workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to create. - */ -ZenWorkspacesStore.prototype.create = async function (record) { - try { - this._validateRecord(record); - const workspace = { - uuid: record.id, - name: record.name, - icon: record.icon, - default: record.default, - containerTabId: record.containerTabId, - position: record.position, - theme: record.theme_type - ? { - type: record.theme_type, - gradientColors: JSON.parse(record.theme_colors), - opacity: record.theme_opacity, - rotation: record.theme_rotation, - texture: record.theme_texture, - } - : null, - }; - await ZenWorkspacesStorage.saveWorkspace(workspace, false); - } catch (error) { - this._log.error(`Error creating workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Updates an existing workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to update. - */ -ZenWorkspacesStore.prototype.update = async function (record) { - try { - this._validateRecord(record); - await this.create(record); // Reuse create for update - } catch (error) { - this._log.error(`Error updating workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Removes a workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to remove. - */ -ZenWorkspacesStore.prototype.remove = async function (record) { - try { - await ZenWorkspacesStorage.removeWorkspace(record.id, false); - } catch (error) { - this._log.error(`Error removing workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Wipes all workspaces from the storage. - */ -ZenWorkspacesStore.prototype.wipe = async function () { - try { - await ZenWorkspacesStorage.wipeAllWorkspaces(); - } catch (error) { - this._log.error('Error wiping all workspaces', error); - throw error; - } -}; - -/** - * Validates a workspace record. - * @param {ZenWorkspaceRecord} record - The workspace record to validate. - */ -ZenWorkspacesStore.prototype._validateRecord = function (record) { - if (!record.id || typeof record.id !== 'string') { - throw new Error('Invalid workspace ID'); - } - if (!record.name || typeof record.name !== 'string') { - throw new Error(`Invalid workspace name for ID ${record.id}`); - } - if (typeof record.default !== 'boolean') { - record.default = false; - } - if (record.icon != null && typeof record.icon !== 'string') { - throw new Error(`Invalid icon for workspace ID ${record.id}`); - } - if (record.containerTabId != null && typeof record.containerTabId !== 'number') { - throw new Error(`Invalid containerTabId for workspace ID ${record.id}`); - } - if (record.position != null && typeof record.position !== 'number') { - throw new Error(`Invalid position for workspace ID ${record.id}`); - } - - // Validate theme properties if they exist - if (record.theme_type) { - if (typeof record.theme_type !== 'string') { - throw new Error(`Invalid theme_type for workspace ID ${record.id}`); - } - if (!record.theme_colors || typeof record.theme_colors !== 'string') { - throw new Error(`Invalid theme_colors for workspace ID ${record.id}`); - } - try { - JSON.parse(record.theme_colors); - } catch (e) { - throw new Error( - `Invalid theme_colors JSON for workspace ID ${record.id}. Error: ${e.message}` - ); - } - if (record.theme_opacity != null && typeof record.theme_opacity !== 'number') { - throw new Error(`Invalid theme_opacity for workspace ID ${record.id}`); - } - if (record.theme_rotation != null && typeof record.theme_rotation !== 'number') { - throw new Error(`Invalid theme_rotation for workspace ID ${record.id}`); - } - if (record.theme_texture != null && typeof record.theme_texture !== 'number') { - throw new Error(`Invalid theme_texture for workspace ID ${record.id}`); - } - } -}; - -/** - * Retrieves changed workspace IDs since the last sync. - * @returns {Object} An object mapping workspace UUIDs to their change timestamps. - */ -ZenWorkspacesStore.prototype.getChangedIDs = async function () { - try { - return await ZenWorkspacesStorage.getChangedIDs(); - } catch (error) { - this._log.error('Error retrieving changed IDs from storage', error); - throw error; - } -}; - -/** - * Clears all recorded changes after a successful sync. - */ -ZenWorkspacesStore.prototype.clearChangedIDs = async function () { - try { - await ZenWorkspacesStorage.clearChangedIDs(); - } catch (error) { - this._log.error('Error clearing changed IDs in storage', error); - throw error; - } -}; - -/** - * Marks a workspace as changed. - * @param {String} uuid - The UUID of the workspace that changed. - */ -ZenWorkspacesStore.prototype.markChanged = async function (uuid) { - try { - await ZenWorkspacesStorage.markChanged(uuid); - } catch (error) { - this._log.error(`Error marking workspace ${uuid} as changed`, error); - throw error; - } -}; - -/** - * Finalizes the store by ensuring all pending operations are completed. - */ -ZenWorkspacesStore.prototype.finalize = async function () { - await Store.prototype.finalize.call(this); -}; - -// Define ZenWorkspacesTracker -function ZenWorkspacesTracker(name, engine) { - Tracker.call(this, name, engine); - this._ignoreAll = false; - - // Observe profile-before-change to stop the tracker gracefully - Services.obs.addObserver(this.asyncObserver, 'profile-before-change'); -} - -ZenWorkspacesTracker.prototype = Object.create(Tracker.prototype); -ZenWorkspacesTracker.prototype.constructor = ZenWorkspacesTracker; - -/** - * Retrieves changed workspace IDs by delegating to the store. - * @returns {Object} An object mapping workspace UUIDs to their change timestamps. - */ -ZenWorkspacesTracker.prototype.getChangedIDs = async function () { - try { - return await this.engine._store.getChangedIDs(); - } catch (error) { - this._log.error('Error retrieving changed IDs from store', error); - throw error; - } -}; - -/** - * Clears all recorded changes after a successful sync. - */ -ZenWorkspacesTracker.prototype.clearChangedIDs = async function () { - try { - await this.engine._store.clearChangedIDs(); - } catch (error) { - this._log.error('Error clearing changed IDs in store', error); - throw error; - } -}; - -/** - * Called when the tracker starts. Registers observers to listen for workspace changes. - */ -ZenWorkspacesTracker.prototype.onStart = function () { - if (this._started) { - return; - } - this._log.trace('Starting tracker'); - // Register observers for workspace changes - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-added'); - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-removed'); - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-updated'); - this._started = true; -}; - -/** - * Called when the tracker stops. Unregisters observers. - */ -ZenWorkspacesTracker.prototype.onStop = function () { - if (!this._started) { - return; - } - this._log.trace('Stopping tracker'); - // Unregister observers for workspace changes - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-added'); - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-removed'); - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-updated'); - this._started = false; -}; - -/** - * Handles observed events and marks workspaces as changed accordingly. - * @param {nsISupports} subject - The subject of the notification. - * @param {String} topic - The topic of the notification. - * @param {String} data - Additional data (JSON stringified array of UUIDs). - */ -ZenWorkspacesTracker.prototype.observe = async function (subject, topic, data) { - if (this.ignoreAll) { - return; - } - - try { - switch (topic) { - case 'profile-before-change': - await this.stop(); - break; - case 'zen-workspace-removed': - case 'zen-workspace-updated': - case 'zen-workspace-added': { - let workspaceIDs; - if (data) { - try { - workspaceIDs = JSON.parse(data); - if (!Array.isArray(workspaceIDs)) { - throw new Error('Parsed data is not an array'); - } - } catch (parseError) { - this._log.error(`Failed to parse workspace UUIDs from data: ${data}`, parseError); - return; - } - } else { - this._log.error(`No data received for event ${topic}`); - return; - } - - this._log.trace(`Observed ${topic} for UUIDs: ${workspaceIDs.join(', ')}`); - - // Process each UUID - for (const workspaceID of workspaceIDs) { - if (typeof workspaceID === 'string') { - // Inform the store about the change - await this.engine._store.markChanged(workspaceID); - } else { - this._log.warn(`Invalid workspace ID encountered: ${workspaceID}`); - } - } - - // Bump the score once after processing all changes - if (workspaceIDs.length > 0) { - this.score += SCORE_INCREMENT_XLARGE; - } - break; - } - } - } catch (error) { - this._log.error(`Error handling ${topic} in observe method`, error); - } -}; - -/** - * Finalizes the tracker by ensuring all pending operations are completed. - */ -ZenWorkspacesTracker.prototype.finalize = async function () { - await Tracker.prototype.finalize.call(this); -}; - -// Define ZenWorkspacesEngine -function ZenWorkspacesEngine(service) { - SyncEngine.call(this, 'Workspaces', service); -} - -ZenWorkspacesEngine.prototype = Object.create(SyncEngine.prototype); -ZenWorkspacesEngine.prototype.constructor = ZenWorkspacesEngine; - -ZenWorkspacesEngine.prototype._storeObj = ZenWorkspacesStore; -ZenWorkspacesEngine.prototype._trackerObj = ZenWorkspacesTracker; -ZenWorkspacesEngine.prototype._recordObj = ZenWorkspaceRecord; -ZenWorkspacesEngine.prototype.version = 2; - -ZenWorkspacesEngine.prototype.syncPriority = 10; -ZenWorkspacesEngine.prototype.allowSkippedRecord = false; - -Object.setPrototypeOf(ZenWorkspacesEngine.prototype, SyncEngine.prototype); diff --git a/src/zen/workspaces/create-workspace-form.css b/src/zen/workspaces/create-workspace-form.css index 33d79eca7b..35d14e0215 100644 --- a/src/zen/workspaces/create-workspace-form.css +++ b/src/zen/workspaces/create-workspace-form.css @@ -33,7 +33,7 @@ zen-workspace-creation { width: calc(100% - 10px); margin: auto; gap: 3.2rem; - margin-top: 1.2rem; + margin-top: 0.6rem; height: 100%; & .zen-workspace-creation-form { diff --git a/src/zen/workspaces/jar.inc.mn b/src/zen/workspaces/jar.inc.mn index 1e4fd273f3..4ab11e7ab2 100644 --- a/src/zen/workspaces/jar.inc.mn +++ b/src/zen/workspaces/jar.inc.mn @@ -7,7 +7,6 @@ content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs) content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs) content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs) - content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs) content/browser/zen-components/ZenGradientGenerator.mjs (../../zen/workspaces/ZenGradientGenerator.mjs) * content/browser/zen-styles/zen-workspaces.css (../../zen/workspaces/zen-workspaces.css) content/browser/zen-styles/zen-gradient-generator.css (../../zen/workspaces/zen-gradient-generator.css) \ No newline at end of file diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css index 71e29815f3..9bbcd7a114 100644 --- a/src/zen/workspaces/zen-workspaces.css +++ b/src/zen/workspaces/zen-workspaces.css @@ -181,7 +181,11 @@ height: calc(100% - var(--zen-toolbox-padding) * 2); } - :root:not([zen-private-window]) & { + :root[zen-private-window] & { + pointer-events: none; + } + + :root:not([zen-unsynced-window]) & { &:hover, &[open='true'] { &::before { @@ -228,15 +232,19 @@ font-weight: 600; align-items: center; margin: 0; + + :root[zen-unsynced-window] & { + pointer-events: none; + } } .zen-workspaces-actions { margin-left: auto !important; - opacity: 0; - visibility: collapse; transition: opacity 0.1s; order: 5; --toolbarbutton-inner-padding: 6px !important; + opacity: 0; + visibility: collapse; & image { border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important; @@ -248,9 +256,24 @@ :root[zen-renaming-tab='true'] & { display: none; } + + :root[zen-unsynced-window] & { + .toolbarbutton-text { + display: flex; + font-size: 10px; + min-height: 22px; + } + + .toolbarbutton-icon { + display: none; + } + } } - :root:not([zen-private-window]) &:hover .zen-workspaces-actions, + :root[zen-unsynced-window='true'] + #navigator-toolbox[zen-has-implicit-hover='true'] + & .zen-workspaces-actions, + :root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions, & .zen-workspaces-actions[open='true'] { visibility: visible; pointer-events: auto; diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js index 197089441a..30374b9ffe 100644 --- a/src/zen/zen.globals.js +++ b/src/zen/zen.globals.js @@ -20,13 +20,11 @@ export default [ 'gZenWorkspaces', 'gZenKeyboardShortcutsManager', 'ZenWorkspacesEngine', - 'ZenWorkspacesStorage', 'ZenWorkspaceBookmarksStorage', 'ZEN_KEYSET_ID', 'gZenPinnedTabManager', - 'ZenPinnedTabsStorage', 'gZenEmojiPicker', 'gZenSessionStore',