This persists the collapsed state of each window's card in the Open Tabs sidebar panel across browser restarts. A new module `SidebarCollapsedWindows.sys.mjs` owns the in-memory map and the `sidebar.openTabsPanel.collapsedWindows` pref. It hydrates lazily, drops entries when a window closes, and dispatches a `CollapsedWindowsChanged` event so panels in other windows re-render. Each panel just calls into the module to read and write state. This gives the pref a single owner so concurrent panels do not race. Private windows are skipped throughout. Preserving collapsed state across close-and-reopen of an individual window (which receives a new `__SSi` from SessionStore) is intentionally out of scope. I will file that as a follow-up. Differential Revision: https://phabricator.services.mozilla.com/D307997
186 lines
5.3 KiB
JavaScript
186 lines
5.3 KiB
JavaScript
/* 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
|
});
|
|
|
|
const PREF = "sidebar.openTabsPanel.collapsedWindows";
|
|
|
|
/**
|
|
* Process-wide owner of the per-window collapsed state for the sidebar Open
|
|
* Tabs panel.
|
|
*
|
|
* State is keyed by the SessionStore window id (`window.__SSi`) and persisted
|
|
* to the `sidebar.openTabsPanel.collapsedWindows` pref as a JSON object whose
|
|
* keys are window ids and whose values are `true` for collapsed cards. Entries
|
|
* are dropped when a window closes or when a hydration sweep finds an entry
|
|
* whose window is no longer open. Private windows never write to the pref.
|
|
*
|
|
* Consumers add a listener for the `CollapsedWindowsChanged` event and call
|
|
* `requestUpdate()` (or equivalent) on every dispatch. The pref is the single
|
|
* source of truth.
|
|
*/
|
|
class SidebarCollapsedWindowsImpl extends EventTarget {
|
|
#map = new Map();
|
|
#initialized = false;
|
|
#observer = null;
|
|
|
|
#ensureInitialized() {
|
|
if (this.#initialized) {
|
|
return;
|
|
}
|
|
this.#initialized = true;
|
|
this.#hydrate();
|
|
const self = this;
|
|
this.#observer = {
|
|
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
|
|
observe(subject, topic) {
|
|
if (topic === "domwindowclosed") {
|
|
self.#onWindowClosed(subject);
|
|
} else if (topic === "quit-application-granted") {
|
|
Services.obs.removeObserver(self.#observer, "domwindowclosed");
|
|
Services.obs.removeObserver(
|
|
self.#observer,
|
|
"quit-application-granted"
|
|
);
|
|
}
|
|
},
|
|
};
|
|
Services.obs.addObserver(this.#observer, "domwindowclosed");
|
|
Services.obs.addObserver(this.#observer, "quit-application-granted");
|
|
}
|
|
|
|
#hydrate() {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(Services.prefs.getStringPref(PREF, "{}"));
|
|
} catch (ex) {
|
|
parsed = {};
|
|
}
|
|
if (!parsed || typeof parsed !== "object") {
|
|
parsed = {};
|
|
}
|
|
const liveIds = this.#collectLiveWindowIds();
|
|
let trimmed = false;
|
|
for (const [id, collapsed] of Object.entries(parsed)) {
|
|
if (liveIds.has(id) && collapsed) {
|
|
this.#map.set(id, true);
|
|
} else if (!liveIds.has(id)) {
|
|
trimmed = true;
|
|
}
|
|
}
|
|
if (trimmed) {
|
|
this.#persist();
|
|
}
|
|
}
|
|
|
|
#collectLiveWindowIds() {
|
|
const ids = new Set();
|
|
for (const win of lazy.SessionStore.getWindows({ private: false })) {
|
|
const id = lazy.SessionStore.getWindowId(win);
|
|
if (id) {
|
|
ids.add(id);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
#persist() {
|
|
Services.prefs.setStringPref(
|
|
PREF,
|
|
JSON.stringify(Object.fromEntries(this.#map))
|
|
);
|
|
}
|
|
|
|
#setCollapsedById(windowId, collapsed) {
|
|
this.#ensureInitialized();
|
|
if (!windowId) {
|
|
return;
|
|
}
|
|
const win = lazy.SessionStore.getWindowById(windowId);
|
|
if (win && lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
return;
|
|
}
|
|
const wasCollapsed = this.#map.get(windowId) === true;
|
|
if (collapsed === wasCollapsed) {
|
|
return;
|
|
}
|
|
if (collapsed) {
|
|
this.#map.set(windowId, true);
|
|
} else {
|
|
this.#map.delete(windowId);
|
|
}
|
|
this.#persist();
|
|
this.dispatchEvent(new CustomEvent("CollapsedWindowsChanged"));
|
|
}
|
|
|
|
#onWindowClosed(win) {
|
|
if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
return;
|
|
}
|
|
// The user can also quit Firefox by clicking the X on the only browser
|
|
// window. In that path the close itself triggers shutdown, so the
|
|
// `quit-application-granted` notification fires AFTER this observer
|
|
// runs and our unregister never gets a chance. Count the remaining
|
|
// non-private browser windows. At domwindowclosed time the closing
|
|
// window is already gone from the enumerator, so zero means it WAS the
|
|
// last one and Firefox is about to quit. Preserve the entry; the
|
|
// hydration bookkeeping on next launch drops it if no window comes back
|
|
// with the same id.
|
|
if (lazy.SessionStore.getWindows({ private: false }).length === 0) {
|
|
return;
|
|
}
|
|
const id = lazy.SessionStore.getWindowId(win);
|
|
if (!id || !this.#map.has(id)) {
|
|
return;
|
|
}
|
|
this.#map.delete(id);
|
|
this.#persist();
|
|
this.dispatchEvent(new CustomEvent("CollapsedWindowsChanged"));
|
|
}
|
|
|
|
isCollapsed(win) {
|
|
this.#ensureInitialized();
|
|
if (!win || lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
return false;
|
|
}
|
|
const id = lazy.SessionStore.getWindowId(win);
|
|
return id ? this.#map.get(id) === true : false;
|
|
}
|
|
|
|
collapseWindow(win) {
|
|
if (!win) {
|
|
return;
|
|
}
|
|
const id = lazy.SessionStore.getWindowId(win);
|
|
if (id) {
|
|
this.#setCollapsedById(id, true);
|
|
}
|
|
}
|
|
|
|
expandWindow(win) {
|
|
if (!win) {
|
|
return;
|
|
}
|
|
const id = lazy.SessionStore.getWindowId(win);
|
|
if (id) {
|
|
this.#setCollapsedById(id, false);
|
|
}
|
|
}
|
|
|
|
collapseWindowById(windowId) {
|
|
this.#setCollapsedById(windowId, true);
|
|
}
|
|
|
|
expandWindowById(windowId) {
|
|
this.#setCollapsedById(windowId, false);
|
|
}
|
|
}
|
|
|
|
export const SidebarCollapsedWindows = new SidebarCollapsedWindowsImpl();
|