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));
+ }
+ },
};