feat: Allow unload to run instantly and fix closing windows on mac, b=no-bug, c=no-component

This commit is contained in:
mr. m
2025-12-10 18:09:07 +01:00
parent d97404d03b
commit 5ce257de83
7 changed files with 117 additions and 98 deletions

View File

@@ -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: true

View File

@@ -4,3 +4,6 @@
- name: zen.window-sync.enabled
value: true
- name: zen.window-sync.log
value: true

View File

@@ -7,5 +7,4 @@ category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys
category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
# App shutdown consumers
category browser-quit-application-granted resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.uninit
category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit

View File

@@ -1,40 +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/.
// 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 FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json';
export class nsZenSessionFile {
#path = PathUtils.join(PathUtils.profileDir, FILE_NAME);
#sidebar = [];
async read() {
try {
const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE });
this.#sidebar = data.sidebar || [];
} catch {
// File doesn't exist yet, that's fine.
}
}
get sidebar() {
return this.#sidebar;
}
set sidebar(data) {
this.#sidebar = data;
}
async #write(data) {
await IOUtils.writeJSON(this.#path, data, { compress: SHOULD_COMPRESS_FILE });
}
async store() {
const data = { sidebar: this.#sidebar };
await this.#write(data);
}
}

View File

@@ -1,11 +1,13 @@
// 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/.
/* 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, {
nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs',
PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs',
@@ -14,31 +16,67 @@ ChromeUtils.defineESModuleGetters(lazy, {
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';
const OBSERVING = ['browser-window-before-show'];
class nsZenSessionManager {
#file;
/**
* 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 = {};
constructor() {
this.#file = new lazy.nsZenSessionFile();
get data() {
return { ...this.#sidebar };
}
set data(data) {
console.log(data);
this.#sidebar = data;
}
}
export class nsZenSessionManager {
#file;
#sidebarObject = new nsZenSidebarObject();
// Called from SessionComponents.manifest on app-startup
init() {
for (let topic of OBSERVING) {
Services.obs.addObserver(this, topic);
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,
});
}
uninit() {
for (let topic of OBSERVING) {
Services.obs.removeObserver(this, topic);
log(...args) {
if (lazy.gShouldLog) {
console.info('ZenSessionManager:', ...args);
}
}
async readFile() {
await this.#file.read();
console.log(await this.#file.load());
try {
this.#sidebar = (await this.#file.load()) || {};
} catch (e) {
console.error('ZenSessionManager: Failed to read session file', e);
this.#sidebar = {};
}
}
onFileRead(initialState) {
@@ -53,33 +91,18 @@ 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.
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.#file.sidebar;
return { ...this.#sidebarObject.data };
}
set #sidebar(data) {
this.#file.sidebar = data;
}
observe(aSubject, aTopic) {
switch (aTopic) {
case 'browser-window-before-show': // catch new windows
this.#onBeforeBrowserWindowShown(aSubject);
break;
default:
break;
}
}
/** Handles the browser-window-before-show observer notification. */
#onBeforeBrowserWindowShown(aWindow) {
// TODO: Initialize new window
void aWindow;
this.#sidebarObject.data = data;
}
/**
@@ -95,7 +118,10 @@ class nsZenSessionManager {
return;
}
this.#collectWindowData(state);
this.#file.store();
// This would save the data to disk asynchronously.
this.#file.data = this.#sidebar;
this.#file.saveSoon();
this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
}
/**
@@ -153,7 +179,8 @@ class nsZenSessionManager {
}
restoreWindowData(aWindowData) {
const sidebar = this.#file.sidebar;
const sidebar = this.#sidebar;
console.log(sidebar);
if (!sidebar) {
return;
}
@@ -167,25 +194,30 @@ class nsZenSessionManager {
if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
return;
}
this.log('Restoring new window with Zen session data');
aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => {
lazy.SessionSaver.run().then(() => {
lazy.setTimeout(() => {
const state = lazy.SessionStore.getCurrentState(true);
const windows = state.windows || [];
let windowToClone =
windows.find(
(win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced
) || {};
let newWindow = Cu.cloneInto(windowToClone, {});
lazy.setTimeout(() => {
const state = lazy.SessionStore.getCurrentState(true);
const windows = (state.windows || []).find(
(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 || []);
delete newWindow.selected;
const newState = { windows: [newWindow] };
SessionStoreInternal.restoreWindows(aWindow, newState, {
firstWindow: true,
});
resolve();
}
newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []);
delete newWindow.selected;
const newState = { windows: [newWindow] };
this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
SessionStoreInternal.restoreWindows(aWindow, newState, {
firstWindow: true,
});
resolve();
});
});
}

View File

@@ -1,6 +1,6 @@
// 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/.
/* 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';
@@ -14,8 +14,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
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 = ['unload'];
const EVENTS = [
'TabOpen',
'TabClose',
@@ -37,7 +39,7 @@ const EVENTS = [
'TabSelect',
'focus',
'unload',
...INSTANT_EVENTS,
];
// Flags acting as an enum for sync types.
@@ -115,6 +117,12 @@ class nsZenWindowSync {
}
}
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.
@@ -143,6 +151,7 @@ class nsZenWindowSync {
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;
}
@@ -219,6 +228,9 @@ class nsZenWindowSync {
if (!window.gZenStartup.isReady || window.gZenWorkspaces?.privateWindowOrDisabled) {
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.
@@ -813,7 +825,12 @@ class nsZenWindowSync {
on_focus(aEvent) {
const { ownerGlobal: window } = aEvent.target;
if (!window.gBrowser || this.#lastFocusedWindow?.deref() === window) {
if (
!window.gBrowser ||
this.#lastFocusedWindow?.deref() === window ||
window.closing ||
!window.toolbar.visible
) {
return;
}
this.#lastFocusedWindow = new WeakRef(window);

View File

@@ -3,7 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen += [
"ZenSessionFile.sys.mjs",
"ZenSessionManager.sys.mjs",
"ZenWindowSync.sys.mjs",
]