diff --git a/src/ZenSidebarManager.mjs b/src/ZenSidebarManager.mjs index 3851e4e..fbb2320 100644 --- a/src/ZenSidebarManager.mjs +++ b/src/ZenSidebarManager.mjs @@ -79,7 +79,7 @@ var gZenBrowserManagerSidebar = { // relative to avoid the top margin // 20px is the padding let parentRelativeHeight = - parent.getBoundingClientRect().height - parent.getBoundingClientRect().top + sidebar.getBoundingClientRect().top - 20; + parent.getBoundingClientRect().height - parent.getBoundingClientRect().top + sidebar.getBoundingClientRect().top; let minHeight = parseInt(computedStyle.getPropertyValue('min-height').replace('px', '')); if (!this._isDragging) { // Prevent multiple resizes @@ -304,7 +304,6 @@ var gZenBrowserManagerSidebar = { let data = this.sidebarData; let newPos = []; for (let element of this.__dragingElement.parentNode.children) { - console.log(element); let panelId = element.getAttribute('zen-sidebar-id'); newPos.push(panelId); } diff --git a/src/ZenThemesCommon.mjs b/src/ZenThemesCommon.mjs new file mode 100644 index 0000000..b59ef92 --- /dev/null +++ b/src/ZenThemesCommon.mjs @@ -0,0 +1,98 @@ +var ZenThemesCommon = { + kZenOSToSmallName: { + WINNT: 'windows', + Darwin: 'macos', + Linux: 'linux', + }, + + kZenColors: ['#aac7ff', '#74d7cb', '#a0d490', '#dec663', '#ffb787', '#dec1b1', '#ffb1c0', '#ddbfc3', '#f6b0ea', '#d4bbff'], + + get currentOperatingSystem() { + let os = Services.appinfo.OS; + return this.kZenOSToSmallName[os]; + }, + + get browsers() { + return Services.wm.getEnumerator('navigator:browser'); + }, + + get currentBrowser() { + return Services.wm.getMostRecentWindow('navigator:browser'); + }, + + get themesRootPath() { + return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes'); + }, + + get themesDataFile() { + return PathUtils.join(PathUtils.profileDir, 'zen-themes.json'); + }, + + getThemeFolder(themeId) { + return PathUtils.join(this.themesRootPath, themeId); + }, + + async getThemes() { + if (!this.themes) { + if (!(await IOUtils.exists(this.themesDataFile))) { + await IOUtils.writeJSON(this.themesDataFile, {}); + } + + this.themes = await IOUtils.readJSON(this.themesDataFile); + } + return this.themes; + }, + + async getThemePreferences(theme) { + const themePath = PathUtils.join(this.themesRootPath, theme.id, 'preferences.json'); + if (!(await IOUtils.exists(themePath)) || !theme.preferences) { + return []; + } + + const preferences = await IOUtils.readJSON(themePath); + + // compat mode for old preferences, all of them can only be checkboxes + if (typeof preferences === 'object' && !Array.isArray(preferences)) { + console.warn( + `[ZenThemes]: Warning, ${theme.name} uses legacy preferences, please migrate them to the new preferences style, as legacy preferences might be removed at a future release. More information at: https://docs.zen-browser.app/themes-store/themes-marketplace-preferences` + ); + const newThemePreferences = []; + + for (let [entry, label] of Object.entries(preferences)) { + const [_, negation = '', os = '', property] = /(!?)(?:(macos|windows|linux):)?([A-z0-9-_.]+)/g.exec(entry); + const isNegation = negation === '!'; + + if ( + (isNegation && os === this.currentOperatingSystem) || + (os !== '' && os !== this.currentOperatingSystem && !isNegation) + ) { + continue; + } + + newThemePreferences.push({ + property, + label, + type: 'checkbox', + disabledOn: os !== '' ? [os] : [], + }); + } + + return newThemePreferences; + } + + return preferences.filter(({ disabledOn = [] }) => !disabledOn.includes(this.currentOperatingSystem)); + }, + + throttle(mainFunction, delay) { + let timerFlag = null; + + return (...args) => { + if (timerFlag === null) { + mainFunction(...args); + timerFlag = setTimeout(() => { + timerFlag = null; + }, delay); + } + }; + }, +}; diff --git a/src/ZenThemesImporter.mjs b/src/ZenThemesImporter.mjs index 257e2b1..7302f34 100644 --- a/src/ZenThemesImporter.mjs +++ b/src/ZenThemesImporter.mjs @@ -8,14 +8,22 @@ const kZenStylesheetThemeHeader = ` const kenStylesheetFooter = ` /* End of Zen Themes */ `; + var gZenStylesheetManager = { async writeStylesheet(path, themes) { let content = kZenStylesheetThemeHeader; + for (let theme of themes) { + if (theme.enabled !== undefined && !theme.enabled) { + continue; + } content += this.getThemeCSS(theme); } + content += kenStylesheetFooter; - let buffer = new TextEncoder().encode(content); + + const buffer = new TextEncoder().encode(content); + await IOUtils.write(path, buffer); }, @@ -36,8 +44,22 @@ var gZenThemeImporter = new (class { constructor() { console.info('ZenThemeImporter: Initiating Zen theme importer'); try { - window.SessionStore.promiseInitialized.then(() => { + window.SessionStore.promiseInitialized.then(async () => { this.insertStylesheet(); + + const themesWithPreferences = await Promise.all( + Object.values(await ZenThemesCommon.getThemes()).map(async (theme) => { + const preferences = await ZenThemesCommon.getThemePreferences(theme); + + return { + name: theme.name, + enabled: theme.enabled, + preferences, + }; + }) + ); + + this.writeToDom(themesWithPreferences); }); console.info('ZenThemeImporter: Zen theme imported'); } catch (e) { @@ -57,31 +79,9 @@ var gZenThemeImporter = new (class { return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css'); } - get themesRootPath() { - return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes'); - } - - get themesDataFile() { - return PathUtils.join(PathUtils.profileDir, 'zen-themes.json'); - } - - getThemeFolder(theme) { - return PathUtils.join(this.themesRootPath, theme.id); - } - - async getThemes() { - if (!this._themes) { - if (!(await IOUtils.exists(this.themesDataFile))) { - await IOUtils.writeJSON(this.themesDataFile, {}); - } - this._themes = await IOUtils.readJSON(this.themesDataFile); - } - return this._themes; - } - - rebuildThemeStylesheet() { - this._themes = null; - this.updateStylesheet(); + async rebuildThemeStylesheet() { + ZenThemesCommon.themes = null; + await this.updateStylesheet(); } get styleSheetURI() { @@ -92,32 +92,155 @@ var gZenThemeImporter = new (class { } getStylesheetURIForTheme(theme) { - return Services.io.newFileURI(new FileUtils.File(PathUtils.join(this.getThemeFolder(theme), 'chrome.css'))); + return Services.io.newFileURI(new FileUtils.File(PathUtils.join(ZenThemesCommon.getThemeFolder(theme.id), 'chrome.css'))); } - insertStylesheet() { - if (IOUtils.exists(this.styleSheetPath)) { - this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); + async insertStylesheet() { + if (await IOUtils.exists(this.styleSheetPath)) { + await this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); } } - removeStylesheet() { - this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); + async removeStylesheet() { + await this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); } async updateStylesheet() { - this.removeStylesheet(); - await this.writeStylesheet(); - this.insertStylesheet(); + await this.removeStylesheet(); + + const themes = Object.values(await ZenThemesCommon.getThemes()); + await this.writeStylesheet(themes); + + const themesWithPreferences = await Promise.all( + themes.map(async (theme) => { + const preferences = await ZenThemesCommon.getThemePreferences(theme); + + return { + name: theme.name, + enabled: theme.enabled, + preferences, + }; + }) + ); + + this.setDefaults(themesWithPreferences); + this.writeToDom(themesWithPreferences); + + await this.insertStylesheet(); } - async writeStylesheet() { + setDefaults(themesWithPreferences) { + for (const { preferences, enabled } of themesWithPreferences) { + if (enabled !== undefined && !enabled) { + continue; + } + + for (const { type, property, defaultValue } of preferences) { + if (defaultValue === undefined) { + continue; + } + + switch (type) { + case 'checkbox': { + const value = Services.prefs.getBoolPref(property, false); + if (typeof defaultValue !== 'boolean') { + console.log(`[ZenThemesImporter]: Warning, invalid data type received for expected type boolean, skipping.`); + continue; + } + + if (!value) { + Services.prefs.setBoolPref(property, defaultValue); + } + break; + } + + default: { + const value = Services.prefs.getStringPref(property, 'zen-property-no-saved'); + + if (typeof defaultValue !== 'string' && typeof defaultValue !== 'number') { + console.log(`[ZenThemesImporter]: Warning, invalid data type received (${typeof defaultValue}), skipping.`); + continue; + } + + if (value === 'zen-property-no-saved') { + Services.prefs.setStringPref(property, defaultValue.toString()); + } + } + } + } + } + } + + writeToDom(themesWithPreferences) { + const browser = ZenThemesCommon.currentBrowser; + + for (const { enabled, preferences, name } of themesWithPreferences) { + const sanitizedName = `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-z_-]+/g, '')}`; + + if (enabled !== undefined && !enabled) { + const element = browser.document.getElementById(sanitizedName); + + if (element) { + element.remove(); + } + + for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) { + const sanitizedProperty = property?.replaceAll(/\./g, '-'); + + browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`); + } + + continue; + } + + for (const { property, type } of preferences) { + const value = Services.prefs.getStringPref(property, ''); + const sanitizedProperty = property?.replaceAll(/\./g, '-'); + + switch (type) { + case 'dropdown': { + if (value !== '') { + let element = browser.document.getElementById(sanitizedName); + + if (!element) { + element = browser.document.createElement('div'); + + element.style.display = 'none'; + element.setAttribute('id', sanitizedName); + + browser.document.body.appendChild(element); + } + + element.setAttribute(sanitizedProperty, value); + } + break; + } + + case 'string': { + if (value === '') { + browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`); + } else { + browser.document.querySelector(':root').style.setProperty(`--${sanitizedProperty}`, value); + } + break; + } + + default: { + } + } + } + } + } + + async writeStylesheet(themeList) { const themes = []; - this._themes = null; - for (let theme of Object.values(await this.getThemes())) { + ZenThemesCommon.themes = null; + + for (let theme of themeList) { theme._chromeURL = this.getStylesheetURIForTheme(theme).spec; themes.push(theme); } + await gZenStylesheetManager.writeStylesheet(this.styleSheetPath, themes); } })(); diff --git a/src/ZenViewSplitter.mjs b/src/ZenViewSplitter.mjs index 4361fc5..1bac78d 100644 --- a/src/ZenViewSplitter.mjs +++ b/src/ZenViewSplitter.mjs @@ -536,8 +536,9 @@ var gZenViewSplitter = new (class { if (this.currentView < 0) return; const currentTab = window.gBrowser.selectedTab; const tabs = this._data[this.currentView].tabs; - for (const tab of tabs) { - this.handleTabClose({ target: tab, forUnsplit: true }); + // note: This MUST be an index loop, as we are removing tabs from the array + for (let i = tabs.length - 1; i >= 0; i--) { + this.handleTabClose({ target: tabs[i], forUnsplit: true }); } window.gBrowser.selectedTab = currentTab; this.updateSplitViewButton(true); @@ -562,7 +563,7 @@ var gZenViewSplitter = new (class { this.unsplitCurrentView(); return; } - const tabs = gBrowser.tabs; + const tabs = gBrowser.visibleTabs; if (tabs.length < 2) { return; } diff --git a/src/ZenWorkspaces.mjs b/src/ZenWorkspaces.mjs index 2c91f9f..7281e9d 100644 --- a/src/ZenWorkspaces.mjs +++ b/src/ZenWorkspaces.mjs @@ -1,11 +1,12 @@ + var ZenWorkspaces = { - async init() { - let docElement = document.documentElement; - if ( - docElement.getAttribute('chromehidden').includes('toolbar') || - docElement.getAttribute('chromehidden').includes('menubar') || - docElement.hasAttribute('privatebrowsingmode') - ) { + /** + * Stores workspace IDs and their last selected tabs. + */ + _lastSelectedWorkspaceTabs: {}, + + init() { + if (!this.shouldHaveWorkspaces) { console.warn('ZenWorkspaces: !!! ZenWorkspaces is disabled in hidden windows !!!'); return; // We are in a hidden window, don't initialize ZenWorkspaces } @@ -16,8 +17,29 @@ var ZenWorkspaces = { }); }, + get shouldShowIconStrip() { + delete this.shouldShowIconStrip; + this.shouldShowIconStrip = Services.prefs.getBoolPref('zen.workspaces.show-icon-strip', true); + return this.shouldShowIconStrip; + }, + + get shouldHaveWorkspaces() { + delete this.shouldHaveWorkspaces; + let docElement = document.documentElement; + this.shouldHaveWorkspaces = !(docElement.hasAttribute('privatebrowsingmode') + || docElement.getAttribute('chromehidden').includes('toolbar') + || docElement.getAttribute('chromehidden').includes('menubar')); + return this.shouldHaveWorkspaces; + }, + get workspaceEnabled() { - return Services.prefs.getBoolPref('zen.workspaces.enabled', false); + delete this.workspaceEnabled; + this.workspaceEnabled = Services.prefs.getBoolPref('zen.workspaces.enabled', false) && this.shouldHaveWorkspaces; + return this.workspaceEnabled; + }, + + getActiveWorkspaceFromCache() { + return this._workspaceCache.workspaces.find((workspace) => workspace.used); }, // Wrorkspaces saving/loading @@ -47,15 +69,25 @@ var ZenWorkspaces = { } }, + async onWorkspacesIconStripChanged() { + this.shouldShowIconStrip = Services.prefs.getBoolPref('zen.workspaces.show-icon-strip', true); + await this._expandWorkspacesStrip(); + }, + async initializeWorkspaces() { Services.prefs.addObserver('zen.workspaces.enabled', this.onWorkspacesEnabledChanged.bind(this)); - this.initializeWorkspacesButton(); + Services.prefs.addObserver('zen.workspaces.show-icon-strip', this.onWorkspacesIconStripChanged.bind(this)); let file = new FileUtils.File(this._storeFile); if (!file.exists()) { await IOUtils.writeJSON(this._storeFile, {}); } + await this.initializeWorkspacesButton(); if (this.workspaceEnabled) { + this._initializeWorkspaceCreationIcons(); + this._initializeWorkspaceEditIcons(); + this._initializeWorkspaceTabContextMenus(); window.addEventListener('TabClose', this.handleTabClose.bind(this)); + window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this)); let workspaces = await this._workspaces(); if (workspaces.workspaces.length === 0) { await this.createAndSaveWorkspace('Default Workspace', true); @@ -73,9 +105,6 @@ var ZenWorkspaces = { } this.changeWorkspace(activeWorkspace, true); } - this._initializeWorkspaceCreationIcons(); - this._initializeWorkspaceEditIcons(); - this._initializeWorkspaceTabContextMenus(); } }, @@ -91,12 +120,12 @@ var ZenWorkspaces = { if (tabs.length === 1) { this._createNewTabForWorkspace({ uuid: workspaceID }); // We still need to close other tabs in the workspace - this.changeWorkspace({ uuid: workspaceID }); + this.changeWorkspace({ uuid: workspaceID }, true); } } }, - _kIcons: ['🏠', '📄', '💹', '💼', '📧', '✅', '👥'], + _kIcons: JSON.parse(Services.prefs.getStringPref("zen.workspaces.icons")).map((icon) => icon), _initializeWorkspaceCreationIcons() { let container = document.getElementById('PanelUI-zen-workspaces-create-icons-container'); @@ -160,6 +189,7 @@ var ZenWorkspaces = { console.info('ZenWorkspaces: Removing workspace', windowID); await this.changeWorkspace(json.workspaces.find((workspace) => workspace.uuid !== windowID)); this._deleteAllTabsInWorkspace(windowID); + delete this._lastSelectedWorkspaceTabs[windowID]; json.workspaces = json.workspaces.filter((workspace) => workspace.uuid !== windowID); await this.unsafeSaveWorkspaces(json); await this._propagateWorkspaceData(); @@ -219,20 +249,41 @@ var ZenWorkspaces = { return workspace.name[0].toUpperCase(); }, - async _propagateWorkspaceData() { + async _propagateWorkspaceData({ + ignoreStrip = false + } = {}) { let currentContainer = document.getElementById('PanelUI-zen-workspaces-current-info'); let workspaceList = document.getElementById('PanelUI-zen-workspaces-list'); + if (!ignoreStrip) { + await this._expandWorkspacesStrip(); + } const createWorkspaceElement = (workspace) => { let element = document.createXULElement('toolbarbutton'); element.className = 'subviewbutton'; element.setAttribute('tooltiptext', workspace.name); element.setAttribute('zen-workspace-id', workspace.uuid); - //element.setAttribute("context", "zenWorkspaceActionsMenu"); + if (workspace.used) { + element.setAttribute('active', 'true'); + } + if (workspace.default) { + element.setAttribute('default', 'true'); + } + const containerGroup = ContextualIdentityService.getPublicIdentities().find( + (container) => container.userContextId === workspace.containerTabId + ); + if (containerGroup) { + element.classList.add('identity-color-' + containerGroup.color); + element.setAttribute('data-usercontextid', containerGroup.userContextId); + } let childs = window.MozXULElement.parseXULToFragment(`
-
-
+ +
+
+
+
+
@@ -241,6 +292,11 @@ var ZenWorkspaces = { // use text content instead of innerHTML to avoid XSS childs.querySelector('.zen-workspace-icon').textContent = this.getWorkspaceIcon(workspace); childs.querySelector('.zen-workspace-name').textContent = workspace.name; + if (containerGroup) { + childs.querySelector('.zen-workspace-container').textContent = ContextualIdentityService.getUserContextLabel( + containerGroup.userContextId + ); + } childs.querySelector('.zen-workspace-actions').addEventListener('command', (event) => { let button = event.target; @@ -290,14 +346,16 @@ var ZenWorkspaces = { } let target = event.target; let panel = document.getElementById('PanelUI-zen-workspaces'); - await this._propagateWorkspaceData(); + await this._propagateWorkspaceData({ + ignoreStrip: true + }); PanelMultiView.openPopup(panel, target, { position: 'bottomright topright', triggerEvent: event, }).catch(console.error); }, - initializeWorkspacesButton() { + async initializeWorkspacesButton() { if (!this.workspaceEnabled) { return; } else if (document.getElementById('zen-workspaces-button')) { @@ -305,14 +363,60 @@ var ZenWorkspaces = { button.removeAttribute('hidden'); 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); + const nextSibling = document.getElementById('zen-sidepanel-button'); + const wrapper = document.createXULElement('toolbarbutton'); + wrapper.id = 'zen-workspaces-button'; + nextSibling.after(wrapper); + await this._expandWorkspacesStrip(); + }, + + async _expandWorkspacesStrip() { + let workspaces = await this._workspaces(); + let workspaceList = document.getElementById('zen-workspaces-button'); + const newWorkspacesButton = document.createXULElement(this.shouldShowIconStrip ? 'hbox' : 'toolbarbutton'); + newWorkspacesButton.id = 'zen-workspaces-button'; + newWorkspacesButton.setAttribute('removable', 'false'); + newWorkspacesButton.setAttribute('showInPrivateBrowsing', 'false'); + newWorkspacesButton.setAttribute('tooltiptext', 'Workspaces'); + + if (this.shouldShowIconStrip) { + for (let workspace of workspaces.workspaces) { + let button = document.createXULElement('toolbarbutton'); + button.className = 'subviewbutton'; + button.setAttribute('tooltiptext', workspace.name); + button.setAttribute('zen-workspace-id', workspace.uuid); + if (workspace.used) { + button.setAttribute('active', 'true'); + } + if (workspace.default) { + button.setAttribute('default', 'true'); + } + button.onclick = (async (_, event) => { + // Make sure it's not a context menu event + if (event.button !== 0) { + return; + } + await this.changeWorkspace(workspace); + }).bind(this, workspace); + let icon = document.createXULElement('div'); + icon.className = 'zen-workspace-icon'; + icon.textContent = this.getWorkspaceIcon(workspace); + button.appendChild(icon); + newWorkspacesButton.appendChild(button); + } + // Listen for context menu events and open the all workspaces dialog + newWorkspacesButton.addEventListener('contextmenu', (event) => { + event.preventDefault(); + this.openWorkspacesDialog(event); + }); + } + + workspaceList.after(newWorkspacesButton); + workspaceList.remove(); + + if (!this.shouldShowIconStrip) { + await this._updateWorkspacesButton(); + } }, async _updateWorkspacesButton() { @@ -322,20 +426,33 @@ var ZenWorkspaces = { } let activeWorkspace = (await this._workspaces()).workspaces.find((workspace) => workspace.used); if (activeWorkspace) { - button.innerHTML = ` -
-
-
-
- `; + button.setAttribute('as-button', 'true'); + button.classList.add('toolbarbutton-1', 'zen-sidebar-action-button'); + + button.addEventListener('click', this.openWorkspacesDialog.bind(this)); + + const wrapper = document.createXULElement('hbox'); + wrapper.className = 'zen-workspace-sidebar-wrapper'; + + const icon = document.createElement('div'); + icon.className = 'zen-workspace-sidebar-icon'; + icon.textContent = this.getWorkspaceIcon(activeWorkspace); + // use text content instead of innerHTML to avoid XSS - button.querySelector('.zen-workspace-sidebar-name').textContent = activeWorkspace.name; - button.querySelector('.zen-workspace-sidebar-icon').textContent = this.getWorkspaceIcon(activeWorkspace); + const name = document.createElement('div'); + name.className = 'zen-workspace-sidebar-name'; + name.textContent = activeWorkspace.name; if (!this.workspaceHasIcon(activeWorkspace)) { - button.querySelector('.zen-workspace-sidebar-icon').setAttribute('no-icon', 'true'); + icon.setAttribute('no-icon', 'true'); } + + wrapper.appendChild(icon); + wrapper.appendChild(name); + + button.innerHTML = ''; + button.appendChild(wrapper); } }, @@ -442,24 +559,39 @@ var ZenWorkspaces = { button.removeAttribute('disabled'); }, + get _shouldAllowPinTab() { + return Services.prefs.getBoolPref('zen.workspaces.individual-pinned-tabs'); + }, + + get tabContainer() { + delete this.tabContainer; + return (this.tabContainer = document.getElementById("tabbrowser-tabs")); + }, + async changeWorkspace(window, onInit = false) { if (!this.workspaceEnabled) { return; } + this.tabContainer._invalidateCachedTabs(); + const shouldAllowPinnedTabs = this._shouldAllowPinTab; let firstTab = undefined; let workspaces = await this._workspaces(); for (let workspace of workspaces.workspaces) { + if (workspace.uuid === window.uuid && workspace.used) { + // If the workspace is already active, do nothing + return; + } workspace.used = workspace.uuid === window.uuid; } - this.unsafeSaveWorkspaces(workspaces); + await this.unsafeSaveWorkspaces(workspaces); console.info('ZenWorkspaces: Changing workspace to', window.uuid); for (let tab of gBrowser.tabs) { - if ((tab.getAttribute('zen-workspace-id') === window.uuid && !tab.pinned) || !tab.hasAttribute('zen-workspace-id')) { + if ((tab.getAttribute('zen-workspace-id') === window.uuid && !(tab.pinned && !shouldAllowPinnedTabs)) || !tab.hasAttribute('zen-workspace-id')) { if (!firstTab) { firstTab = tab; } else if (gBrowser.selectedTab === tab) { // If the selected tab is already in the workspace, we don't want to change it - firstTab = undefined; + firstTab = null; // note: Do not add "undefined" here, a new tab would be created } gBrowser.showTab(tab); if (!tab.hasAttribute('zen-workspace-id')) { @@ -470,21 +602,26 @@ var ZenWorkspaces = { } } if (firstTab) { - gBrowser.selectedTab = firstTab; + gBrowser.selectedTab = this._lastSelectedWorkspaceTabs[window.uuid] ?? firstTab; } if (typeof firstTab === 'undefined' && !onInit) { this._createNewTabForWorkspace(window); } for (let tab of gBrowser.tabs) { if (tab.getAttribute('zen-workspace-id') !== window.uuid) { - gBrowser.hideTab(tab); + // FOR UNLOADING TABS: + // gBrowser.discardBrowser(tab, true); + gBrowser.hideTab(tab, undefined, shouldAllowPinnedTabs); } } + this.tabContainer._invalidateCachedTabs(); document.documentElement.setAttribute('zen-workspace-id', window.uuid); await this.saveWorkspaces(); await this._updateWorkspacesButton(); await this._propagateWorkspaceData(); await this._updateWorkspacesChangeContextMenu(); + + document.getElementById('tabbrowser-tabs')._positionPinnedTabs(); }, async _updateWorkspacesChangeContextMenu() { @@ -530,6 +667,19 @@ var ZenWorkspaces = { await this.changeWorkspace(workspaceData); }, + async onTabBrowserInserted(event) { + let tab = event.originalTarget; + if (tab.getAttribute('zen-workspace-id') || !this.workspaceEnabled) { + return; + } + let workspaces = await this._workspaces(); + let activeWorkspace = workspaces.workspaces.find((workspace) => workspace.used); + if (!activeWorkspace) { + return; + } + tab.setAttribute('zen-workspace-id', activeWorkspace.uuid); + }, + async onLocationChange(browser) { let tab = gBrowser.getTabForBrowser(browser); let workspaceID = tab.getAttribute('zen-workspace-id'); @@ -540,7 +690,9 @@ var ZenWorkspaces = { return; } tab.setAttribute('zen-workspace-id', activeWorkspace.uuid); + workspaceID = activeWorkspace.uuid; } + this._lastSelectedWorkspaceTabs[workspaceID] = tab; }, // Context menu management @@ -575,6 +727,15 @@ var ZenWorkspaces = { } }, + async contextChangeContainerTab(event) { + let workspaces = await this._workspaces(); + let workspace = workspaces.workspaces.find((workspace) => workspace.uuid === this._contextMenuId); + let userContextId = parseInt(event.target.getAttribute('data-usercontextid')); + workspace.containerTabId = userContextId; + await this.saveWorkspace(workspace); + await this._propagateWorkspaceData(); + }, + onContextMenuClose() { let target = document.querySelector( `#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions` @@ -637,12 +798,37 @@ var ZenWorkspaces = { async changeTabWorkspace(workspaceID) { const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab]; + const previousWorkspaceID = document.documentElement.getAttribute('zen-workspace-id'); for (let tab of tabs) { tab.setAttribute('zen-workspace-id', workspaceID); + if (this._lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) { + // This tab is no longer the last selected tab in the previous workspace because it's being moved to + // the current workspace + delete this._lastSelectedWorkspaceTabs[previousWorkspaceID]; + } } const workspaces = await this._workspaces(); await this.changeWorkspace(workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)); }, + + // Tab browser utilities + createContainerTabMenu(event) { + let window = event.target.ownerGlobal; + const workspace = this._workspaceCache.workspaces.find((workspace) => this._contextMenuId === workspace.uuid); + let containerTabId = workspace.containerTabId; + return window.createUserContextMenu(event, { + isContextMenu: true, + excludeUserContextId: containerTabId, + showDefaultTab: true, + }); + }, + + getContextIdIfNeeded(userContextId) { + if (typeof userContextId !== 'undefined' || !this.workspaceEnabled) { + return [userContextId, false]; + } + const activeWorkspace = this.getActiveWorkspaceFromCache(); + return [activeWorkspace?.containerTabId, true]; + }, }; -ZenWorkspaces.init(); diff --git a/src/actors/ZenThemeMarketplaceParent.sys.mjs b/src/actors/ZenThemeMarketplaceParent.sys.mjs index 443f7c0..b92abea 100644 --- a/src/actors/ZenThemeMarketplaceParent.sys.mjs +++ b/src/actors/ZenThemeMarketplaceParent.sys.mjs @@ -8,6 +8,7 @@ export class ZenThemeMarketplaceParent extends JSWindowActorParent { case 'ZenThemeMarketplace:InstallTheme': { console.info('ZenThemeMarketplaceParent: Updating themes'); const theme = message.data.theme; + theme.enabled = true; const themes = await this.getThemes(); themes[theme.id] = theme; this.updateThemes(themes); @@ -73,6 +74,7 @@ export class ZenThemeMarketplaceParent extends JSWindowActorParent { } if (!this.compareversion(themeInfo.version, theme.version || '0.0.0') && themeInfo.version != theme.version) { console.info('ZenThemeMarketplaceParent: Theme update found', theme.id, theme.version, themeInfo.version); + themeInfo.enabled = theme.enabled; updates.push(themeInfo); await this.removeTheme(theme.id, false); this._themes[themeInfo.id] = themeInfo;