diff --git a/src/ZenCompactMode.mjs b/src/ZenCompactMode.mjs index 97fe7d1..01a5521 100644 --- a/src/ZenCompactMode.mjs +++ b/src/ZenCompactMode.mjs @@ -1,5 +1,6 @@ var gZenCompactModeManager = { _flashTimeouts: {}, + _evenListeners: [], init() { Services.prefs.addObserver('zen.view.compact', this._updateEvent.bind(this)); @@ -34,7 +35,12 @@ var gZenCompactModeManager = { return this._sidebar; }, + addEventListener(callback) { + this._evenListeners.push(callback); + }, + _updateEvent() { + this._evenListeners.forEach((callback) => callback()); Services.prefs.setBoolPref('zen.view.sidebar-expanded.on-hover', false); }, @@ -112,9 +118,9 @@ var gZenCompactModeManager = { addMouseActions() { for (let i = 0; i < this.hoverableElements.length; i++) { - const target = this.hoverableElements[i].element; - target.addEventListener('mouseenter', (event) => { - target.setAttribute('zen-user-hover', 'true'); + this.hoverableElements[i].addEventListener('mouseenter', (event) => { + let target = this.hoverableElements[i]; + target.setAttribute('zen-has-hover', 'true'); }); if (this.hoverableElements[i].keepHoverDuration) { diff --git a/src/ZenKeyboardShortcuts.mjs b/src/ZenKeyboardShortcuts.mjs index 7138be2..8fb5a11 100644 --- a/src/ZenKeyboardShortcuts.mjs +++ b/src/ZenKeyboardShortcuts.mjs @@ -10,6 +10,7 @@ const kZKSActions = { // Workspace actions zenChangeWorkspace: ['ZenWorkspaces.changeWorkspaceShortcut()', 'zen-change-workspace', 'workspace-action'], + zenChangeWorkspaceBack: ['ZenWorkspaces.changeWorkspaceShortcut(-1)', 'zen-change-workspace-back', 'workspace-action'], // manage actions openNewTab: ['command:cmd_newNavigatorTabNoEvent', 'open-new-tab', 'tab-action'], @@ -129,7 +130,8 @@ const kZenDefaultShortcuts = { zenSplitViewClose: 'Ctrl+Alt+U', // Workspace actions - zenChangeWorkspace: 'Ctrl+Shift+E', + zenChangeWorkspace: 'Ctrl+E', + zenChangeWorkspaceBack: 'Ctrl+Shift+E', // Compact mode actions zenToggleCompactMode: 'Ctrl+Alt+C', diff --git a/src/ZenSidebarManager.mjs b/src/ZenSidebarManager.mjs index fbb2320..ea81268 100644 --- a/src/ZenSidebarManager.mjs +++ b/src/ZenSidebarManager.mjs @@ -133,6 +133,7 @@ var gZenBrowserManagerSidebar = { } const button = document.getElementById('zen-sidepanel-button'); + if (!button) return; if (Services.prefs.getBoolPref('zen.sidebar.enabled')) { button.removeAttribute('hidden'); } else { @@ -187,6 +188,7 @@ var gZenBrowserManagerSidebar = { _updateSidebarButton() { let button = document.getElementById('zen-sidepanel-button'); + if (!button) return; if (!document.getElementById('zen-sidebar-web-panel').hasAttribute('hidden')) { button.setAttribute('open', 'true'); } else { diff --git a/src/ZenThemesImporter.mjs b/src/ZenThemesImporter.mjs index 7302f34..d62fcd2 100644 --- a/src/ZenThemesImporter.mjs +++ b/src/ZenThemesImporter.mjs @@ -106,6 +106,11 @@ var gZenThemeImporter = new (class { } async updateStylesheet() { + if (Services.focus.activeWindow !== window) { + return; + } + + console.log('ZenThemeImporter: Updating Zen themes'); await this.removeStylesheet(); const themes = Object.values(await ZenThemesCommon.getThemes()); @@ -172,60 +177,60 @@ var gZenThemeImporter = new (class { } writeToDom(themesWithPreferences) { - const browser = ZenThemesCommon.currentBrowser; + for (const browser of ZenThemesCommon.browsers) { + for (const { enabled, preferences, name } of themesWithPreferences) { + const sanitizedName = `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-z_-]+/g, '')}`; - 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 (enabled !== undefined && !enabled) { - const element = browser.document.getElementById(sanitizedName); + if (element) { + element.remove(); + } - 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 } of preferences.filter(({ type }) => type !== 'checkbox')) { + for (const { property, type } of preferences) { + const value = Services.prefs.getStringPref(property, ''); const sanitizedProperty = property?.replaceAll(/\./g, '-'); - browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`); - } + switch (type) { + case 'dropdown': { + if (value !== '') { + let element = browser.document.getElementById(sanitizedName); - continue; - } + if (!element) { + element = browser.document.createElement('div'); - for (const { property, type } of preferences) { - const value = Services.prefs.getStringPref(property, ''); - const sanitizedProperty = property?.replaceAll(/\./g, '-'); + element.style.display = 'none'; + element.setAttribute('id', sanitizedName); - switch (type) { - case 'dropdown': { - if (value !== '') { - let element = browser.document.getElementById(sanitizedName); + browser.document.body.appendChild(element); + } - if (!element) { - element = browser.document.createElement('div'); - - element.style.display = 'none'; - element.setAttribute('id', sanitizedName); - - browser.document.body.appendChild(element); + element.setAttribute(sanitizedProperty, value); } - - element.setAttribute(sanitizedProperty, value); + break; } - break; - } - case 'string': { - if (value === '') { - browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`); - } else { - browser.document.querySelector(':root').style.setProperty(`--${sanitizedProperty}`, value); + case 'string': { + if (value === '') { + browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`); + } else { + browser.document.querySelector(':root').style.setProperty(`--${sanitizedProperty}`, value); + } + break; } - break; - } - default: { + default: { + } } } } diff --git a/src/ZenViewSplitter.mjs b/src/ZenViewSplitter.mjs index 1bac78d..d0782a3 100644 --- a/src/ZenViewSplitter.mjs +++ b/src/ZenViewSplitter.mjs @@ -97,10 +97,13 @@ var gZenViewSplitter = new (class { for (const tab of this._data[this.currentView].tabs) { this.resetTabState(tab, true); } + this.removeSplitters(); this.currentView = -1; this.tabBrowserPanel.removeAttribute('zen-split-view'); this.tabBrowserPanel.style.gridTemplateAreas = ''; + this.tabBrowserPanel.style.gridTemplateColumns = ''; + this.tabBrowserPanel.style.gridTemplateRows = ''; } /** @@ -168,6 +171,10 @@ var gZenViewSplitter = new (class { return this._tabBrowserPanel; } + get minResizeWidth() { + return Services.prefs.getIntPref('zen.splitView.min-resize-width'); + } + /** * Splits a link in a new tab. */ @@ -283,6 +290,8 @@ var gZenViewSplitter = new (class { } this.tabBrowserPanel.removeAttribute('zen-split-view'); this.tabBrowserPanel.style.gridTemplateAreas = ''; + this.tabBrowserPanel.style.gridTemplateColumns = ''; + this.tabBrowserPanel.style.gridTemplateRows = ''; this.setTabsDocShellState(this._data[this.currentView].tabs, false); this.currentView = -1; } @@ -302,6 +311,9 @@ var gZenViewSplitter = new (class { this.setTabsDocShellState(splitData.tabs, true); this.updateSplitViewButton(false); + this.updateGridSizes(splitData); + this.applySplitters(splitData.widths.length , splitData.heights.length); + this.applyGridSizes(); } /** @@ -322,6 +334,65 @@ var gZenViewSplitter = new (class { }); } + /** + * Adds splitters to tabBrowserPanel + * + * @param nrOfColumns number of columns in the grid + * @param nrOfRows number of rows in the grid + */ + applySplitters(nrOfColumns, nrOfRows) { + this.removeSplitters(); + const vSplittersNeeded = (nrOfColumns - 1) * nrOfRows; + const hSplittersNeeded = nrOfRows - 1; + + const insertSplitter = (i, orient, gridIdx) => { + const splitter = document.createElement('div'); + splitter.className = 'zen-split-view-splitter'; + splitter.setAttribute('orient', orient); + splitter.setAttribute('gridIdx', gridIdx); + splitter.style.gridArea = `${orient === 'vertical' ? 'v' : 'h'}Splitter${i}`; + splitter.addEventListener('mousedown', this.handleSplitterMouseDown); + this.tabBrowserPanel.insertAdjacentElement("afterbegin", splitter); + } + for (let i = 1; i <= vSplittersNeeded; i++) { + insertSplitter(i, 'vertical', Math.floor((i - 1) /nrOfRows) + 1); + } + for (let i = 1; i <= hSplittersNeeded; i++) { + insertSplitter(i, 'horizontal', i); + } + } + + /** + * Initialize splitData with default widths and heights if dimensions of grid don't match + * + * @param {object} splitData - The split data. + */ + updateGridSizes(splitData) { + const tabs = splitData.tabs; + const gridType = splitData.gridType; + + let nrOfWidths = 1; + let nrOfHeights = 1; + if (gridType === 'vsep') { + nrOfWidths = tabs.length; + } else if (gridType === 'hsep') { + nrOfHeights = tabs.length; + } else if (gridType === 'grid') { + nrOfWidths = tabs.length > 2 ? Math.ceil(tabs.length / 2) : 2; + nrOfHeights = tabs.length > 2 ? 2 : 1; + } + if (splitData.widths?.length !== nrOfWidths || splitData.heights?.length !== nrOfHeights) { + splitData.widths = Array(nrOfWidths).fill(100 / nrOfWidths); + splitData.heights = Array(nrOfHeights).fill(100 / nrOfHeights); + } + } + + removeSplitters() { + [...gZenViewSplitter.tabBrowserPanel.children] + .filter(e => e.classList.contains('zen-split-view-splitter')) + .forEach(s => s.remove()); + } + /** * Calculates the grid areas for the tabs. * @@ -334,10 +405,10 @@ var gZenViewSplitter = new (class { return this.calculateGridAreasForGrid(tabs); } if (gridType === 'vsep') { - return `'${tabs.map((_, j) => `tab${j + 1}`).join(' ')}'`; + return `'${tabs.slice(0, -1).map((_, j) => `tab${j + 1} vSplitter${j + 1}`).join(' ')} tab${tabs.length}'`; } if (gridType === 'hsep') { - return tabs.map((_, j) => `'tab${j + 1}'`).join(' '); + return tabs.slice(0, -1).map((_, j) => `'tab${j + 1}' 'hSplitter${j + 1}'`).join(' ') + `'tab${tabs.length}`; } return ''; } @@ -349,24 +420,32 @@ var gZenViewSplitter = new (class { * @returns {string} The calculated grid areas. */ calculateGridAreasForGrid(tabs) { - const rows = ['', '']; - tabs.forEach((_, i) => { - if (i % 2 === 0) { - rows[0] += ` tab${i + 1}`; - } else { - rows[1] += ` tab${i + 1}`; - } - }); - if (tabs.length === 2) { - return "'tab1 tab2'"; + return "'tab1 vSplitter1 tab2'"; } + const rows = ['', '']; + for (let i = 0; i < tabs.length - 2; i++) { + if (i % 2 === 0) { + rows[0] += ` tab${i + 1} vSplitter${i + 1}`; + } else { + rows[1] += ` tab${i + 1} vSplitter${i + 1}`; + } + } + for (let i = tabs.length - 2; i < tabs.length; i++) { + if (i % 2 === 0) { + rows[0] += ` tab${i + 1}`; + } else { + rows[1] += ` tab${i + 1}`; + } + } + + let middleColumn = 'hSplitter1 '.repeat(tabs.length - 1); if (tabs.length % 2 !== 0) { - rows[1] += ` tab${tabs.length}`; + rows[1] += ` vSplitter${tabs.length - 1} tab${tabs.length}`; + middleColumn += ` tab${tabs.length}`; } - - return `'${rows[0].trim()}' '${rows[1].trim()}'`; + return `'${rows[0].trim()}' '${middleColumn}' '${rows[1].trim()}'`; } /** @@ -401,6 +480,66 @@ var gZenViewSplitter = new (class { } }; + handleSplitterMouseDown = (event) => { + const splitData = this._data[this.currentView]; + + const isVertical = event.target.getAttribute('orient') === 'vertical'; + const dimension = isVertical ? 'widths' : 'heights'; + const clientAxis = isVertical ? 'screenX' : 'screenY'; + + const gridIdx = event.target.getAttribute('gridIdx'); + let prevPosition = event[clientAxis]; + const dragFunc = (dEvent) => { + requestAnimationFrame(() => { + const movementX = dEvent[clientAxis] - prevPosition; + let percentageChange = (movementX / this.tabBrowserPanel.getBoundingClientRect()[isVertical ? 'width' : 'height']) * 100; + + const currentSize = splitData[dimension][gridIdx - 1]; + const neighborSize = splitData[dimension][gridIdx]; + if (currentSize < this.minResizeWidth && neighborSize < this.minResizeWidth) { + return; + } + let max = false; + if (currentSize + percentageChange < this.minResizeWidth) { + percentageChange = this.minResizeWidth - currentSize; + max = true; + } else if (neighborSize - percentageChange < this.minResizeWidth) { + percentageChange = neighborSize - this.minResizeWidth; + max = true; + } + splitData[dimension][gridIdx - 1] += percentageChange; + splitData[dimension][gridIdx] -= percentageChange; + this.applyGridSizes(); + if (!max) prevPosition = dEvent[clientAxis]; + }); + } + const stopListeners = () => { + removeEventListener('mousemove', dragFunc); + removeEventListener('mouseup', stopListeners); + setCursor('auto'); + } + addEventListener('mousemove', dragFunc); + addEventListener('mouseup', stopListeners); + setCursor(isVertical ? 'ew-resize' : 'n-resize'); + } + + /** + * Applies the grid column and row sizes + */ + applyGridSizes() { + const splitData = this._data[this.currentView]; + const columnGap = 'var(--zen-split-column-gap)'; + const rowGap = 'var(--zen-split-row-gap)'; + + this.tabBrowserPanel.style.gridTemplateColumns = splitData.widths.slice(0, -1).map( + (w) => `calc(${w}% - ${columnGap} * ${splitData.widths.length - 1}/${splitData.widths.length}) ${columnGap}` + ).join(' '); + + this.tabBrowserPanel.style.gridTemplateRows = splitData.heights.slice(0, -1).map( + (h) => `calc(${h}% - ${rowGap} * ${splitData.heights.length - 1}/${splitData.heights.length}) ${rowGap}` + ).join(' '); + } + /** * Sets the docshell state for the tabs. * @@ -583,4 +722,4 @@ var gZenViewSplitter = new (class { : [gBrowser.selectedTab, tabs[nextTabIndex]]; this.splitTabs(selected_tabs, gridType); } -})(); +})(); \ No newline at end of file diff --git a/src/ZenWorkspaces.mjs b/src/ZenWorkspaces.mjs index 7281e9d..4952f57 100644 --- a/src/ZenWorkspaces.mjs +++ b/src/ZenWorkspaces.mjs @@ -515,6 +515,8 @@ var ZenWorkspaces = { icon?.removeAttribute('selected'); await this.createAndSaveWorkspace(workspaceName, false, icon?.label); document.getElementById('PanelUI-zen-workspaces').hidePopup(true); + await this._updateWorkspacesButton(); + await this._propagateWorkspaceData(); }, async saveWorkspaceFromEdit() { @@ -773,12 +775,13 @@ var ZenWorkspaces = { await this.openEditDialog(this._contextMenuId); }, - async changeWorkspaceShortcut() { + async changeWorkspaceShortcut(offset = 1) { // Cycle through workspaces let workspaces = await this._workspaces(); let activeWorkspace = workspaces.workspaces.find((workspace) => workspace.used); let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace); - let nextWorkspace = workspaces.workspaces[workspaceIndex + 1] || workspaces.workspaces[0]; + // note: offset can be negative + let nextWorkspace = workspaces.workspaces[(workspaceIndex + offset + workspaces.workspaces.length) % workspaces.workspaces.length]; this.changeWorkspace(nextWorkspace); },