diff --git a/scripts/recalculate-patches.sh b/scripts/recalculate-patches.sh index 379e3646e..547e2521a 100644 --- a/scripts/recalculate-patches.sh +++ b/scripts/recalculate-patches.sh @@ -11,6 +11,7 @@ npm run import IGNORE_FILES=( "shared.nsh" "ignorePrefs.json" + "toolkit/moz.configure" ) # Recursively find all .patch files in the current directory and its subdirectories diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 81e547928..91bb327d2 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -262,6 +262,18 @@ export class nsZenSessionManager { } return initialState; } + const allowRestoreUnsynced = Services.prefs.getBoolPref( + "zen.session-store.restore-unsynced-windows", + true + ); + if (initialState?.windows?.length && !allowRestoreUnsynced) { + initialState.windows = initialState.windows.filter((win) => { + if (win.isZenUnsynced) { + this.log("Skipping unsynced window during restore"); + } + return !win.isZenUnsynced; + }); + } // 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. @@ -300,27 +312,18 @@ export class nsZenSessionManager { // 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. - const allowRestoreUnsynced = Services.prefs.getBoolPref( - "zen.session-store.restore-unsynced-windows", - true - ); if (!this._shouldRunMigration) { this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`); for (let i = 0; i < initialState.windows.length; i++) { let winData = initialState.windows[i]; if (winData.isZenUnsynced) { - if (!allowRestoreUnsynced) { - // We don't wan't to restore any unsynced windows with the sidebar data. - this.log("Skipping restore of unsynced window"); - delete initialState.windows[i]; - } continue; } this.#restoreWindowData(winData); } } else if (initialState) { this.log("Saving windata state after migration"); - this.saveState(Cu.cloneInto(initialState, {})); + this.saveState(Cu.cloneInto(initialState, {}), true); } delete this._shouldRunMigration; } @@ -438,8 +441,11 @@ export class nsZenSessionManager { * Saves the current session state. Collects data and writes to disk. * * @param {object} state The current session state. + * @param {boolean} soon Whether to save the file soon or immediately. + * If true, the file will be saved asynchronously or when quitting + * the app. If false, the file will be saved immediately. */ - saveState(state) { + saveState(state, soon = false) { let windows = state?.windows || []; windows = windows.filter((win) => this.#isWindowSaveable(win)); if (!windows.length) { @@ -448,11 +454,14 @@ export class nsZenSessionManager { return; } this.#collectWindowData(windows); - // This would save the data to disk asynchronously or when - // quitting the app. + // This would save the data to disk asynchronously or when quitting the app. let sidebar = this.#sidebar; this.#file.data = sidebar; - this.#file.saveSoon(); + if (soon) { + this.#file.saveSoon(); + } else { + this.#file._save(); + } this.#debounceRegeneration(); this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`); } @@ -533,7 +542,7 @@ export class nsZenSessionManager { return; } this.log("Saving closed window session data into Zen session store"); - this.saveState({ windows: [aWinData] }); + this.saveState({ windows: [aWinData] }, true); } /** diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index cba9444c3..75d3e6a01 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -28,7 +28,7 @@ XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter(lazy, "gShouldLog", "zen.window-sync.log", true); const OBSERVING = ["browser-window-before-show", "sessionstore-windows-restored"]; -const INSTANT_EVENTS = ["SSWindowClosing"]; +const INSTANT_EVENTS = ["SSWindowClosing", "TabSelect", "focus"]; const UNSYNCED_WINDOW_EVENTS = ["TabOpen"]; const EVENTS = [ "TabClose", @@ -50,9 +50,6 @@ const EVENTS = [ "ZenTabRemovedFromSplit", "ZenSplitViewTabsSplit", - "TabSelect", - - "focus", ...INSTANT_EVENTS, ...UNSYNCED_WINDOW_EVENTS, ]; @@ -81,6 +78,13 @@ class nsZenWindowSync { lastHandlerPromise: Promise.resolve(), }; + /** + * Promise that resolves when the current docshell swap operation is finished. + * Used to avoid multiple simultaneous swap operations that could interfere with each other. + * For example, when focusing a window AND selecting a tab at the same time. + */ + #docShellSwitchPromise = Promise.resolve(); + /** * Map of sync handlers for different event types. * Each handler is a function that takes the event as an argument. @@ -814,15 +818,17 @@ class nsZenWindowSync { }; }); - await promiseToWait; - this.#createPseudoImageForBrowser(otherBrowser, mySrc); - this.#maybeRemovePseudoImageForBrowser(ourBrowser); - ourBrowser.removeAttribute("zen-pseudo-hidden"); - otherBrowser.setAttribute("zen-pseudo-hidden", "true"); + let promise = this.#createPseudoImageForBrowser(otherBrowser, mySrc); + await Promise.all([promiseToWait, promise]); + aOurTab.ownerGlobal.requestAnimationFrame(() => { + otherBrowser.setAttribute("zen-pseudo-hidden", "true"); + ourBrowser.removeAttribute("zen-pseudo-hidden"); + this.#maybeRemovePseudoImageForBrowser(ourBrowser); + }); callback(); } else { - this.#maybeRemovePseudoImageForBrowser(ourBrowser); ourBrowser.removeAttribute("zen-pseudo-hidden"); + this.#maybeRemovePseudoImageForBrowser(ourBrowser); } resolve(); @@ -837,10 +843,25 @@ class nsZenWindowSync { */ #createPseudoImageForBrowser(aBrowser, aSrc) { const doc = aBrowser.ownerDocument; + const win = aBrowser.ownerGlobal; const img = doc.createElement("img"); img.className = "zen-pseudo-browser-image"; img.src = aSrc; + let promise = new Promise((resolve) => { + if (img.complete) { + resolve(); + return; + } + let finish = () => { + win.requestAnimationFrame(() => { + resolve(); + }); + }; + img.onload = finish; + img.onerror = finish; + }); aBrowser.after(img); + return promise; } /** @@ -914,14 +935,8 @@ class nsZenWindowSync { * * @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; - } + async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { let activeBrowsers = aWindow.gBrowser.selectedBrowsers; let activeTabs = activeBrowsers.map((browser) => aWindow.gBrowser.getTabForBrowser(browser)); // Ignore previous tabs that are still "active". These scenarios could happen for example, @@ -1214,11 +1229,12 @@ class nsZenWindowSync { }); } - on_focus(aEvent) { + async on_focus(aEvent) { if (typeof aEvent.target !== "object") { return; } - const { ownerGlobal: window } = aEvent.target; + await this.#docShellSwitchPromise; + const window = Services.focus.activeWindow; if ( !window?.gBrowser || this.#lastFocusedWindow?.deref() === window || @@ -1229,17 +1245,21 @@ class nsZenWindowSync { } this.#lastFocusedWindow = new WeakRef(window); this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab); - return this.#onTabSwitchOrWindowFocus(window); + return (this.#docShellSwitchPromise = this.#onTabSwitchOrWindowFocus(window)); } - on_TabSelect(aEvent) { + async on_TabSelect(aEvent) { + await this.#docShellSwitchPromise; 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); + return (this.#docShellSwitchPromise = this.#onTabSwitchOrWindowFocus( + aEvent.target.ownerGlobal, + previousTab + )); } on_SSWindowClosing(aEvent) { @@ -1270,7 +1290,7 @@ class nsZenWindowSync { // If the page has a title, set it. When doing a swap and we still didn't // flush the tab state, the title might not be correct. - if (activePageData) { + if (activePageData && win.gBrowser) { win.gBrowser.setInitialTabTitle(tab, activePageData.title, { isContentTitle: activePageData.title && activePageData.title != activePageData.url, }); @@ -1375,7 +1395,7 @@ class nsZenWindowSync { return new Promise((resolve) => { lazy.setTimeout(() => { - this.#onTabSwitchOrWindowFocus(window, null, /* ignoreSameTab = */ true).finally(resolve); + this.#onTabSwitchOrWindowFocus(window, null).finally(resolve); }, 0); }); }