diff --git a/prefs/browser.yaml b/prefs/browser.yaml index 30aa603e9c..f76c33f5c8 100644 --- a/prefs/browser.yaml +++ b/prefs/browser.yaml @@ -3,7 +3,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - name: browser.startup.page - value: 3 + value: 0 + locked: true - name: browser.sessionstore.restore_pinned_tabs_on_demand value: true diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 91b136f2ec..0ae64eab21 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -58,3 +58,4 @@ + diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index fc39083763..ab73371949 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -42,6 +42,7 @@ content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs) content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs) content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs) + content/browser/zen-components/ZenWindowSyncing.mjs (../../zen/workspaces/ZenWindowSyncing.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) diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs new file mode 100644 index 0000000000..c50960ae10 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -0,0 +1,27 @@ +// 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 FILE_NAME = 'zen-sessions.jsonlz4'; + +export class nsZenSessionFile { + #path; + + #windows; + + constructor() { + this.#path = PathUtils.join(profileDir, FILE_NAME); + } + + async read() { + try { + return await IOUtils.readJSON(this.#path, { compress: true }); + } catch (e) { + return {}; + } + } + + async write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: true }); + } +} diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs new file mode 100644 index 0000000000..68aa829e40 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -0,0 +1,50 @@ +// 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 { + cancelIdleCallback, + clearTimeout, + requestIdleCallback, + setTimeout, +} from 'resource://gre/modules/Timer.sys.mjs'; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', + RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', +}); + +class nsZenSessionManager { + #file; + + constructor() { + this.#file = null; + } + + get file() { + if (!this.#file) { + this.#file = lazy.ZenSessionFile; + } + return this.#file; + } + + /** + * Saves the current session state. Collects data and writes to disk. + * + * @param forceUpdateAllWindows (optional) + * Forces us to recollect data for all windows and will bypass and + * update the corresponding caches. + */ + saveState(forceUpdateAllWindows = false) { + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Don't save (or even collect) anything in permanent private + // browsing mode + return Promise.resolve(); + } + } +} + +export const ZenSessionStore = new nsZenSessionManager(); diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs new file mode 100644 index 0000000000..4600702349 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionWindow.sys.mjs @@ -0,0 +1,35 @@ +// 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/. + +export class ZenSessionWindow { + #id; + #selectedWorkspace; + #selectedTab; + + constructor(id) { + this.#id = id; + this.#selectedWorkspace = null; + this.#selectedTab = null; + } + + get id() { + return this.#id; + } + + get selectedWorkspace() { + return this.#selectedWorkspace; + } + + set selectedWorkspace(workspace) { + this.#selectedWorkspace = workspace; + } + + get selectedTab() { + return this.#selectedTab; + } + + set selectedTab(tab) { + this.#selectedTab = tab; + } +} diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index f6e7b44f5f..4366090a2f 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -97,6 +97,7 @@ } 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 { @@ -1511,6 +1512,7 @@ } async onTabLabelChanged(tab) { + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); if (!this._pinsCache) { return; } diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs new file mode 100644 index 0000000000..857f9fc9cf --- /dev/null +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -0,0 +1,308 @@ +// 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/. +{ + class nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature { + #ignoreNextEvents = false; + #waitForPromise = null; + + constructor() { + super(); + if (!window.closed) { + this.init(); + } + } + + async init() { + await gZenWorkspaces.promiseInitialized; + this.#makeSureAllTabsHaveIds(); + this.#setUpEventListeners(); + } + + #makeSureAllTabsHaveIds() { + const allTabs = gZenWorkspaces.allStoredTabs; + for (const tab of allTabs) { + if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) { + const tabId = gZenUIManager.generateUuidv4(); + tab.setAttribute('zen-sync-id', tabId); + } + } + } + + #setUpEventListeners() { + const kEvents = [ + 'TabClose', + 'TabOpen', + 'TabMove', + + 'TabPinned', + 'TabUnpinned', + + 'TabAddedToEssentials', + 'TabRemovedFromEssentials', + + 'TabHide', + 'TabShow', + + 'ZenTabIconChanged', + 'ZenTabLabelChanged', + + 'TabGroupCreate', + 'TabGroupRemoved', + 'TabGrouped', + 'TabUngrouped', + 'TabGroupMoved', + ]; + const eventListener = this.#handleEvent.bind(this); + for (const event of kEvents) { + window.addEventListener(event, eventListener); + } + + window.addEventListener('unload', () => { + for (const event of kEvents) { + window.removeEventListener(event, eventListener); + } + }); + } + + #handleEvent(event) { + this.#propagateToOtherWindows(event); + } + + async #propagateToOtherWindows(event) { + if (this.#ignoreNextEvents) { + return; + } + if (this.#waitForPromise) { + await this.#waitForPromise; + } + this.#waitForPromise = new Promise((resolve) => { + this.foreachWindowAsActive(async (browser) => { + if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { + await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); + } + }).then(() => { + resolve(); + }); + }); + } + + async onExternalTabEvent(event) { + this.#ignoreNextEvents = true; + switch (event.type) { + case 'TabClose': + this.#onTabClose(event); + break; + case 'TabOpen': + await this.#onTabOpen(event); + break; + case 'TabPinned': + this.#onTabPinned(event); + break; + case 'TabUnpinned': + this.#onTabUnpinned(event); + break; + case 'TabAddedToEssentials': + this.#onTabAddedToEssentials(event); + break; + case 'TabRemovedFromEssentials': + this.#onTabRemovedFromEssentials(event); + break; + case 'TabHide': + this.#onTabHide(event); + break; + case 'TabShow': + this.#onTabShow(event); + break; + case 'TabMove': + case 'TabGroupMoved': + this.#onTabMove(event); + break; + case 'ZenTabIconChanged': + this.#onTabIconChanged(event); + break; + case 'ZenTabLabelChanged': + this.#onTabLabelChanged(event); + break; + case 'TabGroupCreate': + this.#onTabGroupCreate(event); + break; + case 'TabGroupRemoved': + case 'TabGrouped': + case 'TabUngrouped': + // Tab grouping changes are automatically synced by Firefox + break; + default: + console.warn(`Unhandled event type: ${event.type}`); + break; + } + this.#ignoreNextEvents = false; + } + + #getTabId(tab) { + return tab.getAttribute('zen-sync-id'); + } + + #getTabWithId(tabId) { + for (const tab of gZenWorkspaces.allStoredTabs) { + if (this.#getTabId(tab) === tabId) { + return tab; + } + } + return null; + } + + #onTabClose(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToClose = this.#getTabWithId(tabId); + if (tabToClose) { + gBrowser.removeTab(tabToClose); + } + } + + #onTabPinned(event) { + const targetTab = event.target; + if (targetTab.hasAttribute('zen-essential')) { + return this.#onTabAddedToEssentials(event); + } + const tabId = this.#getTabId(targetTab); + const elementIndex = targetTab.elementIndex; + const tabToPin = this.#getTabWithId(tabId); + if (tabToPin) { + gBrowser.pinTab(tabToPin); + gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group }); + } + } + + #onTabUnpinned(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToUnpin = this.#getTabWithId(tabId); + if (tabToUnpin) { + gBrowser.unpinTab(tabToUnpin); + } + } + + #onTabIconChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #onTabLabelChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #updateTabIconAndLabel(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToChange = this.#getTabWithId(tabId); + if (tabToChange && tabToChange.hasAttribute('pending')) { + gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); + gBrowser._setTabLabel(tabToChange, targetTab.label); + } + } + + #onTabAddedToEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToAdd = this.#getTabWithId(tabId); + if (tabToAdd) { + gZenPinnedTabManager.addToEssentials(tabToAdd); + } + } + + #onTabRemovedFromEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToRemove = this.#getTabWithId(tabId); + if (tabToRemove) { + gZenPinnedTabManager.removeFromEssentials(tabToRemove); + } + } + + #onTabHide(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToHide = this.#getTabWithId(tabId); + if (tabToHide) { + gBrowser.hideTab(tabToHide); + } + } + + #onTabShow(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToShow = this.#getTabWithId(tabId); + if (tabToShow) { + gBrowser.showTab(tabToShow); + } + } + + #onTabMove(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToMove = this.#getTabWithId(tabId); + const workspaceId = targetTab.getAttribute('zen-workspace-id'); + const isEssential = targetTab.hasAttribute('zen-essential'); + if (tabToMove) { + let tabSibling = targetTab.previousElementSibling; + let isFirst = false; + if (!tabSibling?.hasAttribute('zen-sync-id')) { + isFirst = true; + } + gBrowser.zenHandleTabMove(tabToMove, () => { + if (isFirst) { + let container; + if (isEssential) { + container = gZenWorkspaces.getEssentialsSection(tabToMove); + } else { + const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); + container = tabToMove.pinned + ? workspaceElement.pinnedTabsContainer + : workspaceElement.tabsContainer; + } + container.insertBefore(tabToMove, container.firstChild); + } else { + let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => { + return this.#getTabId(tab) === this.#getTabId(tabSibling); + }); + if (relativeTab) { + relativeTab.after(tabToMove); + } + } + }); + } + } + + async #onTabOpen(event) { + const targetTab = event.target; + const isPinned = targetTab.pinned; + const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); + if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) { + const tabId = gZenUIManager.generateUuidv4(); + targetTab.setAttribute('zen-sync-id', tabId); + } + const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { + createLazyBrowser: true, + essential: isEssential, + pinned: isPinned, + }); + if (!isEssential) { + gZenWorkspaces.moveTabToWorkspace( + duplicatedTab, + targetTab.getAttribute('zen-workspace-id') + ); + } + duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); + duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); + } + + #onTabGroupCreate(event) { + const targetGroup = event.target; + const isSplitView = targetGroup.classList.contains('zen-split-view'); + const isFolder = targetGroup.isZenFolder; + } + } + + window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync(); +}