diff --git a/src/ZenWorkspaces.mjs b/src/ZenWorkspaces.mjs index 2146da2..728547f 100644 --- a/src/ZenWorkspaces.mjs +++ b/src/ZenWorkspaces.mjs @@ -305,11 +305,10 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { async _propagateWorkspaceData({ ignoreStrip = false } = {}) { await this.foreachWindowAsActive(async (browser) => { - let currentContainer = browser.document.getElementById('PanelUI-zen-workspaces-current-info'); let workspaceList = browser.document.getElementById('PanelUI-zen-workspaces-list'); const createWorkspaceElement = (workspace) => { let element = browser.document.createXULElement('toolbarbutton'); - element.className = 'subviewbutton'; + element.className = 'subviewbutton zen-workspace-button'; element.setAttribute('tooltiptext', workspace.name); element.setAttribute('zen-workspace-id', workspace.uuid); if (this.isWorkspaceActive(workspace)) { @@ -334,6 +333,15 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
+
+ + + + + + +
+ @@ -341,6 +349,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { // use text content instead of innerHTML to avoid XSS childs.querySelector('.zen-workspace-icon').textContent = browser.ZenWorkspaces.getWorkspaceIcon(workspace); + childs.querySelector('.zen-workspace-order-up').addEventListener('click', (async (event) => { + event.stopPropagation(); + event.preventDefault(); + await browser.ZenWorkspaces.moveWorkspaceUp(event, workspace.uuid); + }).bind(browser.ZenWorkspaces)); + childs.querySelector('.zen-workspace-order-down').addEventListener('click', (async (event) => { + event.stopPropagation(); + event.preventDefault(); + await browser.ZenWorkspaces.moveWorkspaceDown(event, workspace.uuid); + }).bind(browser.ZenWorkspaces)); childs.querySelector('.zen-workspace-name').textContent = workspace.name; if (containerGroup) { childs.querySelector('.zen-workspace-container').textContent = ContextualIdentityService.getUserContextLabel( @@ -373,24 +391,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { }; browser.ZenWorkspaces._workspaceCache = null; let workspaces = await browser.ZenWorkspaces._workspaces(); - let activeWorkspace = await browser.ZenWorkspaces.getActiveWorkspace(); - currentContainer.innerHTML = ''; workspaceList.innerHTML = ''; workspaceList.parentNode.style.display = 'flex'; - if (workspaces.workspaces.length - 1 <= 0) { + if (workspaces.workspaces.length <= 0) { workspaceList.innerHTML = 'No workspaces available'; workspaceList.setAttribute('empty', 'true'); } else { workspaceList.removeAttribute('empty'); } - if (activeWorkspace) { - let currentWorkspace = createWorkspaceElement(activeWorkspace); - currentContainer.appendChild(currentWorkspace); - } + for (let workspace of workspaces.workspaces) { - if (browser.ZenWorkspaces.isWorkspaceActive(workspace)) { - continue; - } let workspaceElement = createWorkspaceElement(workspace); workspaceList.appendChild(workspaceElement); } @@ -400,6 +410,38 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { }); } + isLastWorkspace(workspace) { + let workspaces = this._workspaceCache; + return workspaces.workspaces.indexOf(workspace) === workspaces.workspaces.length - 1; + } + + isFirstWorkspace(workspace) { + let workspaces = this._workspaceCache; + return workspaces.workspaces.indexOf(workspace) === 0; + } + + async moveWorkspaceUp(event, uuid) { + await ZenWorkspacesStorage.moveWorkspaceUp(uuid); + await this._propagateWorkspaceData(); + } + + async moveWorkspaceDown(event, uuid) { + await ZenWorkspacesStorage.moveWorkspaceDown(uuid); + await this._propagateWorkspaceData(); + } + + toggleReorderMode () { + const workspacesList = document.getElementById("PanelUI-zen-workspaces-list"); + const reorderModeButton = document.getElementById("PanelUI-zen-workspaces-reorder-mode"); + if(workspacesList.getAttribute('reorder-mode') === 'true' && reorderModeButton.getAttribute('active') === 'true') { + workspacesList.removeAttribute('reorder-mode'); + reorderModeButton.removeAttribute('active'); + } else { + workspacesList.setAttribute('reorder-mode', 'true'); + reorderModeButton.setAttribute('active', 'true'); + } + } + async openWorkspacesDialog(event) { if (!this.workspaceEnabled) { return; @@ -426,96 +468,116 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { await this._expandWorkspacesStrip(); } - async _expandWorkspacesStrip(browser = undefined) { - if (typeof browser === 'undefined' || typeof browser.ZenWorkspaces === 'undefined') { + async _expandWorkspacesStrip(browser = window) { + if (typeof browser.ZenWorkspaces === 'undefined') { browser = window; } - let workspaces = await browser.ZenWorkspaces._workspaces(); - let workspaceList = browser.document.getElementById('zen-workspaces-button'); - const newWorkspacesButton = browser.document.createXULElement('toolbarbutton'); - newWorkspacesButton.id = 'zen-workspaces-button'; - newWorkspacesButton.setAttribute('removable', 'true'); - newWorkspacesButton.setAttribute('showInPrivateBrowsing', 'false'); - newWorkspacesButton.setAttribute('tooltiptext', 'Workspaces'); + let button = browser.document.getElementById('zen-workspaces-button'); + + if (!button) { + button = browser.document.createXULElement('toolbarbutton'); + button.id = 'zen-workspaces-button'; + let navbar = browser.document.getElementById('nav-bar'); + navbar.appendChild(button); + } + + while (button.firstChild) { + button.firstChild.remove(); + } + + for (let attr of [...button.attributes]) { + if (attr.name !== 'id') { + button.removeAttribute(attr.name); + } + } + + button.className = ''; + + if (this._workspacesButtonClickListener) { + button.removeEventListener('click', this._workspacesButtonClickListener); + this._workspacesButtonClickListener = null; + } + if (this._workspaceButtonContextMenuListener) { + button.removeEventListener('contextmenu', this._workspaceButtonContextMenuListener); + this._workspaceButtonContextMenuListener = null; + } if (this.shouldShowIconStrip) { + button.setAttribute('removable', 'true'); + button.setAttribute('showInPrivateBrowsing', 'false'); + button.setAttribute('tooltiptext', 'Workspaces'); + + let workspaces = await this._workspaces(); + for (let workspace of workspaces.workspaces) { - let button = browser.document.createXULElement('toolbarbutton'); - button.className = 'subviewbutton'; - button.setAttribute('tooltiptext', workspace.name); - button.setAttribute('zen-workspace-id', workspace.uuid); + let workspaceButton = browser.document.createXULElement('toolbarbutton'); + workspaceButton.className = 'subviewbutton'; + workspaceButton.setAttribute('tooltiptext', workspace.name); + workspaceButton.setAttribute('zen-workspace-id', workspace.uuid); + if (this.isWorkspaceActive(workspace)) { - button.setAttribute('active', 'true'); + workspaceButton.setAttribute('active', 'true'); + } else { + workspaceButton.removeAttribute('active'); } if (workspace.default) { - button.setAttribute('default', 'true'); + workspaceButton.setAttribute('default', 'true'); + } else { + workspaceButton.removeAttribute('default'); } - button.onclick = (async (_, event) => { - // Make sure it's not a context menu event + + workspaceButton.addEventListener('click', async (event) => { if (event.button !== 0) { return; } await this.changeWorkspace(workspace); - }).bind(browser.ZenWorkspaces, workspace); + }); + let icon = browser.document.createXULElement('div'); icon.className = 'zen-workspace-icon'; icon.textContent = this.getWorkspaceIcon(workspace); - button.appendChild(icon); - newWorkspacesButton.appendChild(button); + workspaceButton.appendChild(icon); + button.appendChild(workspaceButton); } - // Listen for context menu events and open the all workspaces dialog - newWorkspacesButton.addEventListener( - 'contextmenu', - ((event) => { - event.preventDefault(); - browser.ZenWorkspaces.openWorkspacesDialog(event); - }).bind(this) - ); - } - workspaceList.after(newWorkspacesButton); - workspaceList.remove(); + this._workspaceButtonContextMenuListener = (event) => { + event.preventDefault(); + browser.ZenWorkspaces.openWorkspacesDialog(event); + }; + button.addEventListener('contextmenu', this._workspaceButtonContextMenuListener); + } else { + let activeWorkspace = await this.getActiveWorkspace(); + if (activeWorkspace) { + button.setAttribute('as-button', 'true'); + button.classList.add('toolbarbutton-1', 'zen-sidebar-action-button'); - if (!this.shouldShowIconStrip) { - await this._updateWorkspacesButton(browser); + this._workspacesButtonClickListener = browser.ZenWorkspaces.openWorkspacesDialog.bind(browser.ZenWorkspaces); + button.addEventListener('click', this._workspacesButtonClickListener); + + const wrapper = browser.document.createXULElement('hbox'); + wrapper.className = 'zen-workspace-sidebar-wrapper'; + + const icon = browser.document.createXULElement('div'); + icon.className = 'zen-workspace-sidebar-icon'; + icon.textContent = this.getWorkspaceIcon(activeWorkspace); + + const name = browser.document.createXULElement('div'); + name.className = 'zen-workspace-sidebar-name'; + name.textContent = activeWorkspace.name; + + if (!this.workspaceHasIcon(activeWorkspace)) { + icon.setAttribute('no-icon', 'true'); + } + + wrapper.appendChild(icon); + wrapper.appendChild(name); + + button.appendChild(wrapper); + } } } - async _updateWorkspacesButton(browser = window) { - let button = browser.document.getElementById('zen-workspaces-button'); - if (!button) { - return; - } - let activeWorkspace = await this.getActiveWorkspace(); - if (activeWorkspace) { - button.setAttribute('as-button', 'true'); - button.classList.add('toolbarbutton-1', 'zen-sidebar-action-button'); - button.addEventListener('click', browser.ZenWorkspaces.openWorkspacesDialog.bind(browser.ZenWorkspaces)); - - const wrapper = browser.document.createXULElement('hbox'); - wrapper.className = 'zen-workspace-sidebar-wrapper'; - - const icon = browser.document.createElement('div'); - icon.className = 'zen-workspace-sidebar-icon'; - icon.textContent = this.getWorkspaceIcon(activeWorkspace); - - // use text content instead of innerHTML to avoid XSS - const name = browser.document.createElement('div'); - name.className = 'zen-workspace-sidebar-name'; - name.textContent = activeWorkspace.name; - - if (!this.workspaceHasIcon(activeWorkspace)) { - icon.setAttribute('no-icon', 'true'); - } - - wrapper.appendChild(icon); - wrapper.appendChild(name); - - button.innerHTML = ''; - button.appendChild(wrapper); - } - } // Workspaces management @@ -575,7 +637,6 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { let icon = document.querySelector('#PanelUI-zen-workspaces-icon-picker [selected]'); icon?.removeAttribute('selected'); await this.createAndSaveWorkspace(workspaceName, false, icon?.label); - await this._updateWorkspacesButton(); await this._propagateWorkspaceData(); this.closeWorkspacesSubView(); } diff --git a/src/ZenWorkspacesStorage.mjs b/src/ZenWorkspacesStorage.mjs index caf62b0..477d5a9 100644 --- a/src/ZenWorkspacesStorage.mjs +++ b/src/ZenWorkspacesStorage.mjs @@ -257,68 +257,6 @@ var ZenWorkspacesStorage = { }); }, - async updateWorkspaceOrder(uuid, newPosition, notifyObservers = true) { - const changedUUIDs = new Set([uuid]); - - await PlacesUtils.withConnectionWrapper('ZenWorkspacesStorage.updateWorkspaceOrder', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - // Get the positions of the workspaces before and after the new position - const neighborPositions = await db.execute(` - SELECT - (SELECT MAX("position") FROM zen_workspaces WHERE "position" < :newPosition) as before_position, - (SELECT MIN("position") FROM zen_workspaces WHERE "position" > :newPosition) as after_position - `, { newPosition }); - - let beforePosition = neighborPositions[0].getResultByName('before_position'); - let afterPosition = neighborPositions[0].getResultByName('after_position'); - - // Calculate the new position - if (beforePosition === null && afterPosition === null) { - // This is the only workspace - newPosition = 1000; - } else if (beforePosition === null) { - // This will be the first workspace - newPosition = Math.floor(afterPosition / 2); - } else if (afterPosition === null) { - // This will be the last workspace - newPosition = beforePosition + 1000; - } else { - // This workspace will be between two others - newPosition = Math.floor((beforePosition + afterPosition) / 2); - } - - // Update the workspace's position - await db.execute(` - UPDATE zen_workspaces - SET "position" = :newPosition - WHERE uuid = :uuid - `, { uuid, newPosition }); - - // Record the change - await db.execute(` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, { - uuid, - timestamp: Math.floor(now / 1000) - }); - - await this.updateLastChangeTimestamp(db); - - // Check if reordering is necessary - if (this.shouldReorderWorkspaces(beforePosition, newPosition, afterPosition)) { - await this.reorderAllWorkspaces(db, changedUUIDs); - } - }); - }); - - if (notifyObservers) { - this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs)); - } - }, - shouldReorderWorkspaces(before, current, after) { const minGap = 1; // Minimum allowed gap between positions return (before !== null && current - before < minGap) || (after !== null && after - current < minGap); @@ -357,4 +295,164 @@ var ZenWorkspacesStorage = { `); return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; }, + + async moveWorkspaceUp(uuid, notifyObservers = true) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenWorkspacesStorage.moveWorkspaceUp', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + // Get the current workspace + const currentWorkspaceRows = await db.execute(` + SELECT uuid, "position" FROM zen_workspaces WHERE uuid = :uuid + `, { uuid }); + + if (currentWorkspaceRows.length === 0) { + // Workspace not found + return; + } + + const currentWorkspace = { + uuid: currentWorkspaceRows[0].getResultByName('uuid'), + position: currentWorkspaceRows[0].getResultByName('position') + }; + + // Find the workspace before this one + const previousWorkspaceRows = await db.execute(` + SELECT uuid, "position" FROM zen_workspaces + WHERE "position" < :currentPosition + ORDER BY "position" DESC + LIMIT 1 + `, { currentPosition: currentWorkspace.position }); + + if (previousWorkspaceRows.length === 0) { + // No previous workspace; already at the top + return; + } + + const previousWorkspace = { + uuid: previousWorkspaceRows[0].getResultByName('uuid'), + position: previousWorkspaceRows[0].getResultByName('position') + }; + + // Swap positions + await db.execute(` + UPDATE zen_workspaces + SET "position" = :newPosition + WHERE uuid = :uuid + `, { uuid: currentWorkspace.uuid, newPosition: previousWorkspace.position }); + + await db.execute(` + UPDATE zen_workspaces + SET "position" = :newPosition + WHERE uuid = :uuid + `, { uuid: previousWorkspace.uuid, newPosition: currentWorkspace.position }); + + // Record the changes + await db.execute(` + INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) + VALUES (:uuid1, :timestamp), (:uuid2, :timestamp) + `, { + uuid1: currentWorkspace.uuid, + uuid2: previousWorkspace.uuid, + timestamp: Math.floor(now / 1000) + }); + + changedUUIDs.add(currentWorkspace.uuid); + changedUUIDs.add(previousWorkspace.uuid); + + await this.updateLastChangeTimestamp(db); + + // Check if reordering is necessary + if (this.shouldReorderWorkspaces(null, previousWorkspace.position, currentWorkspace.position)) { + await this.reorderAllWorkspaces(db, changedUUIDs); + } + }); + }); + + if (notifyObservers) { + this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs)); + } + }, + + async moveWorkspaceDown(uuid, notifyObservers = true) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenWorkspacesStorage.moveWorkspaceDown', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + // Get the current workspace + const currentWorkspaceRows = await db.execute(` + SELECT uuid, "position" FROM zen_workspaces WHERE uuid = :uuid + `, { uuid }); + + if (currentWorkspaceRows.length === 0) { + // Workspace not found + return; + } + + const currentWorkspace = { + uuid: currentWorkspaceRows[0].getResultByName('uuid'), + position: currentWorkspaceRows[0].getResultByName('position') + }; + + // Find the workspace after this one + const nextWorkspaceRows = await db.execute(` + SELECT uuid, "position" FROM zen_workspaces + WHERE "position" > :currentPosition + ORDER BY "position" ASC + LIMIT 1 + `, { currentPosition: currentWorkspace.position }); + + if (nextWorkspaceRows.length === 0) { + // No next workspace; already at the bottom + return; + } + + const nextWorkspace = { + uuid: nextWorkspaceRows[0].getResultByName('uuid'), + position: nextWorkspaceRows[0].getResultByName('position') + }; + + // Swap positions + await db.execute(` + UPDATE zen_workspaces + SET "position" = :newPosition + WHERE uuid = :uuid + `, { uuid: currentWorkspace.uuid, newPosition: nextWorkspace.position }); + + await db.execute(` + UPDATE zen_workspaces + SET "position" = :newPosition + WHERE uuid = :uuid + `, { uuid: nextWorkspace.uuid, newPosition: currentWorkspace.position }); + + // Record the changes + await db.execute(` + INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) + VALUES (:uuid1, :timestamp), (:uuid2, :timestamp) + `, { + uuid1: currentWorkspace.uuid, + uuid2: nextWorkspace.uuid, + timestamp: Math.floor(now / 1000) + }); + + changedUUIDs.add(currentWorkspace.uuid); + changedUUIDs.add(nextWorkspace.uuid); + + await this.updateLastChangeTimestamp(db); + + // Check if reordering is necessary + if (this.shouldReorderWorkspaces(currentWorkspace.position, nextWorkspace.position, null)) { + await this.reorderAllWorkspaces(db, changedUUIDs); + } + }); + }); + + if (notifyObservers) { + this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs)); + } + }, };