diff --git a/src/browser/app/profile/zen-browser.js b/src/browser/app/profile/zen-browser.js index 387141da..7a9aa9fb 100644 --- a/src/browser/app/profile/zen-browser.js +++ b/src/browser/app/profile/zen-browser.js @@ -153,4 +153,4 @@ pref('zen.sidebar.floating', true); pref('zen.splitView.working', false); // Zen Workspaces -pref('zen.workspaces.enabled', false); +pref('zen.workspaces.enabled', true); diff --git a/src/browser/base/content/ZenWorkspaces.mjs b/src/browser/base/content/ZenWorkspaces.mjs index 37f9e774..c300f898 100644 --- a/src/browser/base/content/ZenWorkspaces.mjs +++ b/src/browser/base/content/ZenWorkspaces.mjs @@ -1,6 +1,7 @@ var ZenWorkspaces = { async init() { + console.log("Initializing ZenWorkspaces..."); await this.initializeWorkspaces(); console.log("ZenWorkspaces initialized"); }, @@ -18,29 +19,48 @@ var ZenWorkspaces = { ); }, + async _workspaces() { + if (!this._workspaceCache) { + this._workspaceCache = await IOUtils.readJSON(this._storeFile); + if (!this._workspaceCache.workspaces) { + this._workspaceCache.workspaces = []; + } + } + return this._workspaceCache; + }, + async initializeWorkspaces() { + this.initializeWorkspacesButton(); let file = new FileUtils.File(this._storeFile); if (!file.exists()) { await IOUtils.writeJSON(this._storeFile, {}); } + if (this.workspaceEnabled) { + let workspaces = await this._workspaces(); + console.log("Workspaces loaded", workspaces); + if (workspaces.workspaces.length === 0) { + await this.createAndSaveWorkspace("Default Workspace"); + } else { + let activeWorkspace = workspaces.workspaces.find(workspace => workspace.used); + if (!activeWorkspace) { + activeWorkspace = workspaces.workspaces.find(workspace => workspace.default); + activeWorkspace.used = true; + await this.saveWorkspaces(); + } + await this.changeWorkspace(activeWorkspace); + } + } }, async saveWorkspace(workspaceData) { let json = await IOUtils.readJSON(this._storeFile); - if (!json.workspaces) { + if (typeof json.workspaces === "undefined") { json.workspaces = []; } json.workspaces.push(workspaceData); console.log("Saving workspace", workspaceData); await IOUtils.writeJSON(this._storeFile, json); - }, - - async loadWorkspace(windowID) { - let json = await IOUtils.readJSON(this._storeFile); - if (!json.workspaces) { - return []; - } - return json.workspaces.filter(workspace => workspace.uuid === windowID); + this._workspaceCache = null; }, async removeWorkspace(windowID) { @@ -52,38 +72,175 @@ var ZenWorkspaces = { await IOUtils.writeJSON(this._storeFile, json); }, - async getWorkspaces() { - let json = await IOUtils.readJSON(this._storeFile); - return json; + async saveWorkspaces() { + await IOUtils.writeJSON(this._storeFile, await this._workspaces()); + this._workspaceCache = null; + }, + + async unsafeSaveWorkspaces(workspaces) { + await IOUtils.writeJSON(this._storeFile, workspaces); + this._workspaceCache = null; }, // Workspaces dialog UI management - + async _propagateWorkspaceData() { + let currentContainer = document.getElementById("PanelUI-zen-workspaces-current-info"); + let workspaceList = document.getElementById("PanelUI-zen-workspaces-list"); + const createWorkspaceElement = (workspace) => { + let element = document.createElement("toolbarbutton"); + element.className = "subviewbutton"; + element.setAttribute("tooltiptext", workspace.name); + element.setAttribute("zen-workspace-id", workspace.uuid); + element.innerHTML = ` +
+ ${workspace.name[0].toUpperCase()} +
+
+ ${workspace.name} +
+ `; + element.onclick = (async () => await this.changeWorkspace(workspace)).bind(this, workspace); + return element; + } + let workspaces = await this._workspaces(); + let activeWorkspace = workspaces.workspaces.find(workspace => workspace.used); + currentContainer.innerHTML = ""; + workspaceList.innerHTML = ""; + workspaceList.parentNode.style.display = "flex"; + if (workspaces.workspaces.length - 1 <= 0) { + workspaceList.parentNode.style.display = "none"; + } + if (activeWorkspace) { + let currentWorkspace = createWorkspaceElement(activeWorkspace); + currentContainer.appendChild(currentWorkspace); + } + for (let workspace of workspaces.workspaces) { + if (workspace.used) { + continue; + } + let workspaceElement = createWorkspaceElement(workspace); + workspaceList.appendChild(workspaceElement); + } + }, + + async openWorkspacesDialog(event) { + if (!this.workspaceEnabled) { + return; + } + let target = event.target; + let panel = document.getElementById("PanelUI-zen-workspaces"); + await this._propagateWorkspaceData(); + PanelMultiView.openPopup(panel, target, { + position: "bottomright topright", + triggerEvent: event, + }).catch(console.error); + }, + + initializeWorkspacesButton() { + if (!this.workspaceEnabled) { + return; + } + let browserTabs = document.getElementById("tabbrowser-tabs"); + let button = document.createElement("toolbarbutton"); + button.id = "zen-workspaces-button"; + button.className = "toolbarbutton-1 chromeclass-toolbar-additional"; + button.setAttribute("label", "Workspaces"); + button.setAttribute("tooltiptext", "Workspaces"); + button.onclick = this.openWorkspacesDialog.bind(this); + browserTabs.insertAdjacentElement("beforebegin", button); + }, + + async _updateWorkspacesButton() { + let button = document.getElementById("zen-workspaces-button"); + if (!button) { + return; + } + let activeWorkspace = (await this._workspaces()).workspaces.find(workspace => workspace.used); + if (activeWorkspace) { + button.innerHTML = activeWorkspace.name[0].toUpperCase(); + } + }, // Workspaces management _prepareNewWorkspace(window) { - for (let tab of window.gBrowser.tabs) { - tab.addAttribute("zen-workspace-id", window.uuid); + document.documentElement.setAttribute("zen-workspace-id", window.uuid); + for (let tab of gBrowser.tabs) { + if (!tab.getAttribute("zen-workspace-id")) { + tab.setAttribute("zen-workspace-id", window.uuid); + } } - window.document.documentElement.setAttribute("zen-workspace-id", window.uuid); }, - _createWorkspaceData() { + _createNewTabForWorkspace(window) { + gZenUIManager.openAndChangeToTab(Services.prefs.getStringPref("browser.startup.homepage")); + let tab = gBrowser.selectedTab; + tab.setAttribute("zen-workspace-id", window.uuid); + }, + + async changeWorkspace(window) { + if (!this.workspaceEnabled) { + return; + } + if (document.documentElement.getAttribute("zen-workspace-id") === window.uuid) { + return; + } + let firstTab = undefined; + // Get the number of tabs that are hidden before we start hiding them + let numHiddenTabs = gBrowser.tabs.reduce((acc, tab) => { + return tab.getAttribute("zen-workspace-id") !== window.uuid ? acc + 1 : acc; + }, 0); + if (numHiddenTabs === gBrowser.tabs.length) { + // If all tabs are hidden, we need to create a new tab + // to show the workspace + this._createNewTabForWorkspace(window); + } + for (let tab of gBrowser.tabs) { + if (tab.getAttribute("zen-workspace-id") === window.uuid) { + if (!firstTab) { + firstTab = tab; + } + tab.removeAttribute("hidden"); + } + } + for (let tab of gBrowser.tabs) { + if (tab.getAttribute("zen-workspace-id") !== window.uuid) { + tab.setAttribute("hidden", "true"); + } + } + let workspaces = await this._workspaces(); + for (let workspace of workspaces.workspaces) { + workspace.used = workspace.uuid === window.uuid; + } + this.unsafeSaveWorkspaces(workspaces); + // TODO: Handle the case when there are no tabs in the workspace + gBrowser.selectedTab = firstTab; + document.documentElement.setAttribute("zen-workspace-id", window.uuid); + await this.saveWorkspaces(); + await this._updateWorkspacesButton(); + await this._propagateWorkspaceData(); + }, + + _createWorkspaceData(name) { let window = { uuid: gZenUIManager.generateUuidv4(), default: false, + used: true, icon: "", - name: `New Workspace`, + name: name, }; this._prepareNewWorkspace(window); return window; }, - async createAndSaveWorkspace() { - let workspaceData = this._createWorkspaceData(); + async createAndSaveWorkspace(name = "New Workspace") { + if (!this.workspaceEnabled) { + return; + } + let workspaceData = this._createWorkspaceData(name); await this.saveWorkspace(workspaceData); + await this.changeWorkspace(workspaceData); }, }; diff --git a/src/browser/base/content/zen-locales.inc.xhtml b/src/browser/base/content/zen-locales.inc.xhtml index 71060498..9269e0f5 100644 --- a/src/browser/base/content/zen-locales.inc.xhtml +++ b/src/browser/base/content/zen-locales.inc.xhtml @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/src/browser/base/content/zen-popupset.inc.xhtml b/src/browser/base/content/zen-popupset.inc.xhtml index 0abd8ce6..07a34243 100644 --- a/src/browser/base/content/zen-popupset.inc.xhtml +++ b/src/browser/base/content/zen-popupset.inc.xhtml @@ -75,3 +75,19 @@ + + + + +

+ + +
+ +

+ + +
+ +
+
\ No newline at end of file diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch new file mode 100644 index 00000000..95f9ace9 --- /dev/null +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -0,0 +1,13 @@ +diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs +index 26f5671c849d9b0a126d79b07bc7d3d7870826ec..decc4c975507c9111df78dbc43434fa46d5f5e82 100644 +--- a/browser/components/sessionstore/TabState.sys.mjs ++++ b/browser/components/sessionstore/TabState.sys.mjs +@@ -98,6 +98,8 @@ var TabStateInternal = { + tabData.muteReason = tab.muteReason; + } + ++ tabData.zenWorkspace = tab.getAttribute("zen-workspace-id"); ++ + tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true); + + tabData.userContextId = tab.userContextId || 0; diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 0be59b0d..de327c07 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,8 +1,20 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 3bca0b6d30468dc3a755219723f673ec80dfce6e..200a609ea31a8c904e2d3e39a3a06ff67fb313ed 100644 +index 2674dc2bebf436529a46d45c52cb56e86b82c03f..c70801bed55e0182eb56e9fd7bea5be9986c7120 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -7623,6 +7623,11 @@ var TabContextMenu = { +@@ -3208,6 +3208,11 @@ + ) { + tabWasReused = true; + tab = this.selectedTab; ++ ++ if (tabData.zenWorkspace) { ++ tab.setAttribute("zen-workspace-id", tabData.zenWorkspace); ++ } ++ + if (!tabData.pinned) { + this.unpinTab(tab); + } else { +@@ -7878,6 +7883,11 @@ var TabContextMenu = { this.contextTab.linkedBrowser, document.getElementById("context_sendTabToDevice") ); diff --git a/src/browser/locales/en-US/browser/zen-workspaces.ftl b/src/browser/locales/en-US/browser/zen-workspaces.ftl new file mode 100644 index 00000000..04b2a5fc --- /dev/null +++ b/src/browser/locales/en-US/browser/zen-workspaces.ftl @@ -0,0 +1,8 @@ + +zen-panel-ui-current-window-text = Current Window + +zen-panel-ui-workspaces-text = Other Workspaces + +zen-panel-ui-workspaces-new + .label = New Workspace + .accesskey = N diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index c003fe16..f571ee8d 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -153,7 +153,8 @@ #TabsToolbar #new-tab-button, #appMenu-zoomEnlarge-button2, #PanelUI-zen-profiles-newProfile, -#zen-sidebar-add-panel-button { +#zen-sidebar-add-panel-button, +#PanelUI-zen-workspaces-new { list-style-image: url("plus.svg") !important; } diff --git a/src/browser/themes/shared/zen-panel-ui.css b/src/browser/themes/shared/zen-panel-ui.css index ca5a0098..6d417002 100644 --- a/src/browser/themes/shared/zen-panel-ui.css +++ b/src/browser/themes/shared/zen-panel-ui.css @@ -234,3 +234,85 @@ #zenSplitViewModifierViewDefault .zen-split-view-modifier-preview.grid box:nth-child(3) { grid-area: c; } + +/* Workspaces */ + +#zen-workspaces-button { + border: 1px solid var(--zen-colors-border); + border-radius: 50px; + height: calc(var(--zen-sidebar-action-button-width) - 10px) !important; + margin-bottom: 5px !important; + justify-content: center; + align-items: center; + display: flex; + font-weight: 600; +} + +#PanelUI-zen-workspaces { + width: 300px; + min-height: 200px; +} + +#PanelUI-zen-workspaces > vbox { + width: 100%; + padding: 15px 20px; + position: relative; +} + +#PanelUI-zen-workspaces-list { + display: flex; + flex-direction: column; +} + +#PanelUI-zen-workspaces > vbox > vbox:nth-child(2) { + margin-top: 10px; +} + +#PanelUI-zen-workspaces-current-info toolbarbutton:last-child { + margin-bottom: 0 !important; +} + +#PanelUI-zen-workspaces-list toolbarbutton, +#PanelUI-zen-workspaces-current-info toolbarbutton { + padding: 5px 10px; + border-radius: 5px; + + margin-left: 0 !important; + margin-right: 0 !important; + + display: flex; + align-items: center; + + &:first-child { + margin-top: 10px; + } + + & .zen-workspace-icon { + width: 30px; + height: 30px; + border-radius: 5px; + margin-right: 15px; + border: 1px solid var(--zen-colors-border); + display: flex; + justify-content: center; + align-items: center; + font-weight: 600; + } + + & .zen-workspace-name { + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +#PanelUI-zen-workspaces-new { + position: absolute; + top: 10px; + right: 10px; + padding: 3px; + border-radius: 4px; + width: 20px; + height: 20px; +}