Compare commits

..

3 Commits

Author SHA1 Message Date
mr. m
e73ea97ea0 fix: Fixed zen not starting up on macos, b=closes #11599, c=no-component 2025-12-14 01:47:54 +01:00
salmonumbrella
fb9bbc3a51 feat(downloads): add pref to control download popup position independently, p=#11607
* feat(downloads): add pref to control download popup position independently

Add `zen.downloads.icon-popup-position` preference that allows users to
control the download popup/indicator position independently from the
vertical tabs position.

Valid values:
- "follow-tabs" (default): popup appears on same side as vertical tabs
- "left": popup always appears on the left
- "right": popup always appears on the right

This is useful for users who have vertical tabs on the right but prefer
the download indicator to appear on the left side of the screen.

* feat: Convert pref to integers, b=no-bug, c=no-component

---------

Co-authored-by: salmonumbrella <salmonumbrella@users.noreply.github.com>
Co-authored-by: mr. m <mr.m@tuta.com>
2025-12-14 01:08:13 +01:00
mr. m
bdfb810212 fix: Dont use a new timestamp when changing config dumps, b=closes #11601, c=configs 2025-12-13 21:15:37 +01:00
62 changed files with 2429 additions and 2177 deletions

View File

@@ -15,6 +15,5 @@
"ebay",
"ebay-*"
]
},
"timestamp": 1765455207275
}
}

View File

@@ -66,10 +66,6 @@ zen-panel-ui-gradient-click-to-add = Click to add a color
zen-workspace-creation-name =
.placeholder = Space Name
zen-move-tab-to-workspace-button =
.label = Move To...
.tooltiptext = Move all tabs in this window to a Space
zen-workspaces-panel-context-reorder =
.label = Reorder Spaces

View File

@@ -8,3 +8,12 @@
- name: network.predictor.enable-hover-on-ssl
value: true
# See https://github.com/zen-browser/desktop/issues/11599, this pref seems to
# have disabled itself on macos for some unknown reason.
# Make sure its in sync with:
# https://searchfox.org/firefox-main/rev/1477feb9706f4ccc5bd571c1c215832a6fbb7464/modules/libpref/init/StaticPrefList.yaml#7741-7748
- name: gfx.webrender.compositor
condition: 'defined(XP_WIN) || defined(XP_DARWIN)'
value: '@cond'
mirror: once

View File

@@ -7,3 +7,9 @@
- name: zen.downloads.download-animation-duration
value: 1000 # ms
- name: zen.downloads.icon-popup-position
# 0: Follow tab's position
# 1: Left side always
# 2: Right side always
value: 0

View File

@@ -1,9 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.session-store.backup-file
value: true
- name: zen.session-store.log
value: false

View File

@@ -1,9 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.window-sync.enabled
value: true
- name: zen.window-sync.log
value: false

View File

@@ -45,6 +45,7 @@
# Scripts used all over the browser
<script type="module" src="chrome://browser/content/zen-components/ZenFolder.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenPinnedTabsStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenMediaController.mjs"></script>

View File

@@ -59,8 +59,3 @@
<menuseparator />
<menuitem id="context_zenOpenSiteSettings" data-l10n-id="zen-site-data-site-settings"/>
</menupopup>
<menupopup id="zenMoveTabsToSyncedWorkspacePopup">
# Popup to move tabs to a synced workspace.
# This would be automatically populated with the list of available synced workspaces.
</menupopup>

View File

@@ -1,21 +0,0 @@
diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
@@ -380,7 +381,7 @@ var SessionFileInternal = {
this._readOrigin = result.origin;
result.noFilesFound = noFilesFound;
-
+ await lazy.ZenSessionStore.readFile();
return result;
},

View File

@@ -1,20 +0,0 @@
diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs
index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644
--- a/browser/components/sessionstore/SessionSaver.sys.mjs
+++ b/browser/components/sessionstore/SessionSaver.sys.mjs
@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
/*
@@ -305,6 +306,7 @@ var SessionSaverInternal = {
this._maybeClearCookiesAndStorage(state);
Glean.sessionRestore.collectData.stopAndAccumulate(timerId);
+ lazy.ZenSessionStore.saveState(state);
return this._writeState(state);
},

View File

@@ -1,21 +0,0 @@
diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644
--- a/browser/components/sessionstore/SessionStartup.sys.mjs
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
StartupPerformance:
"resource:///modules/sessionstore/StartupPerformance.sys.mjs",
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
const STATE_RUNNING_STR = "running";
@@ -179,6 +180,8 @@ export var SessionStartup = {
this._initialState = parsed;
}
+ lazy.ZenSessionStore.onFileRead(this._initialState);
+
if (this._initialState == null) {
// No valid session found.
this._sessionType = this.NO_SESSION;

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520bd2b354b8 100644
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb22176c60c4e 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -11,15 +11,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
@@ -1911,6 +1914,8 @@ var SessionStoreInternal = {
@@ -1911,6 +1913,8 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
@@ -28,60 +20,19 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
@@ -2020,6 +2025,10 @@ var SessionStoreInternal = {
this._windows[aWindow.__SSi].isTaskbarTab = true;
}
+ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) {
+ this._windows[aWindow.__SSi].isZenUnsynced = true;
+ }
+
let tabbrowser = aWindow.gBrowser;
// add tab change listeners to all already existing tabs
@@ -2151,7 +2160,6 @@ var SessionStoreInternal = {
if (closedWindowState) {
let newWindowState;
if (
- AppConstants.platform == "macosx" ||
!lazy.SessionStartup.willRestore()
) {
// We want to split the window up into pinned tabs and unpinned tabs.
@@ -2215,6 +2223,15 @@ var SessionStoreInternal = {
});
this._shouldRestoreLastSession = false;
}
+ else if (!aInitialState && isRegularWindow) {
+ let windowPromises = [];
+ for (let window of this._browserWindows) {
+ windowPromises.push(lazy.TabStateFlusher.flushWindow(window));
+ }
+ aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => Promise.all(windowPromises).finally(() => {
+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this, resolve);
+ }));
+ }
if (this._restoreLastWindow && aWindow.toolbar.visible) {
// always reset (if not a popup window)
@@ -2465,7 +2482,7 @@ var SessionStoreInternal = {
// 2) Flush the window.
// 3) When the flush is complete, revisit our decision to store the window
// in _closedWindows, and add/remove as necessary.
- if (!winData.isPrivate && !winData.isTaskbarTab) {
+ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
this.maybeSaveClosedWindow(winData, isLastWindow);
@@ -2384,11 +2388,9 @@ var SessionStoreInternal = {
tabbrowser.selectedTab.label;
}
@@ -2486,7 +2503,7 @@ var SessionStoreInternal = {
- if (AppConstants.platform != "macosx") {
// Until we decide otherwise elsewhere, this window is part of a series
// of closing windows to quit.
winData._shouldRestore = true;
- }
// Save non-private windows if they have at
// least one saveable tab or are the last window.
- if (!winData.isPrivate && !winData.isTaskbarTab) {
+ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
this.maybeSaveClosedWindow(winData, isLastWindow);
if (!isLastWindow && winData.closedId > -1) {
@@ -3373,7 +3390,7 @@ var SessionStoreInternal = {
// Store the window's close date to figure out when each individual tab
// was closed. This timestamp should allow re-arranging data based on how
@@ -3373,7 +3375,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -90,12 +41,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
return;
}
@@ -4089,6 +4106,12 @@ var SessionStoreInternal = {
@@ -4089,6 +4091,12 @@ var SessionStoreInternal = {
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
+ tabState.zenEssential = false;
+ tabState.zenSyncId = null;
+ tabState.zenPinnedId = null;
+ tabState.zenIsGlance = false;
+ tabState.zenGlanceId = null;
+ tabState.zenHasStaticLabel = false;
@@ -103,7 +54,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
@@ -4525,6 +4548,7 @@ var SessionStoreInternal = {
@@ -4525,6 +4533,7 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -111,7 +62,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5374,7 +5398,7 @@ var SessionStoreInternal = {
@@ -5374,7 +5383,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -120,16 +71,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
removableTabs.push(tab);
}
}
@@ -5483,7 +5507,7 @@ var SessionStoreInternal = {
@@ -5434,7 +5443,7 @@ var SessionStoreInternal = {
}
// collect the data for all windows
for (ix in this._windows) {
- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
+ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) {
// window data is still in _statesToRestore
continue;
}
@@ -5625,11 +5649,15 @@ var SessionStoreInternal = {
let workspaceID = aWindow.getWorkspaceID();
- if (workspaceID) {
+ if (workspaceID && !(this.isLastRestorableWindow() && AppConstants.platform == "macosx")) {
winData.workspaceID = workspaceID;
}
},
@@ -5625,11 +5634,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -139,14 +90,19 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
let winData = this._windows[aWindow.__SSi];
let tabsData = (winData.tabs = []);
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
+ winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore();
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5652,7 +5680,7 @@ var SessionStoreInternal = {
@@ -5640,6 +5650,7 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5652,7 +5663,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -155,7 +111,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -5765,8 +5793,8 @@ var SessionStoreInternal = {
@@ -5765,8 +5776,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -166,17 +122,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -5809,6 +5837,9 @@ var SessionStoreInternal = {
@@ -5809,6 +5820,8 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
);
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
+ aWindow.gZenWorkspaces.activeWorkspace = winData.activeZenSpace || null;
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
@@ -6372,6 +6403,25 @@ var SessionStoreInternal = {
@@ -6372,6 +6385,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -190,8 +145,8 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
+ if (tabData.zenHasStaticLabel) {
+ tab.setAttribute("zen-has-static-label", "true");
+ }
+ if (tabData.zenSyncId) {
+ tab.setAttribute("id", tabData.zenSyncId);
+ if (tabData.zenPinnedId) {
+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId);
+ }
+ if (tabData.zenDefaultUserContextId) {
+ tab.setAttribute("zenDefaultUserContextId", true);
@@ -202,7 +157,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -7290,7 +7340,7 @@ var SessionStoreInternal = {
@@ -7290,7 +7322,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -211,7 +166,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..89ae48f816bee85b2870f383723b520b
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7305,7 +7355,7 @@ var SessionStoreInternal = {
@@ -7305,7 +7337,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;

View File

@@ -1,13 +1,13 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb1525c623 100644
index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
@@ -85,7 +85,24 @@ class _TabState {
@@ -85,7 +85,22 @@ class _TabState {
tabData.groupId = tab.group.id;
}
+ tabData.zenWorkspace = tab.getAttribute("zen-workspace-id");
+ tabData.zenSyncId = tab.getAttribute("id");
+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id");
+ tabData.zenEssential = tab.getAttribute("zen-essential");
+ tabData.pinned = tabData.pinned || tabData.zenEssential;
+ tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId");
@@ -17,8 +17,6 @@ index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb
+ tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label");
+ tabData.zenGlanceId = tab.getAttribute("glance-id");
+ tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab");
+ tabData._zenPinnedInitialState = tab._zenPinnedInitialState;
+ tabData._zenIsActiveTab = tab._zenContentsVisible;
+
tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true);
+ if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) {
@@ -27,12 +25,3 @@ index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb
tabData.userContextId = tab.userContextId || 0;
@@ -98,7 +115,7 @@ class _TabState {
// Copy data from the tab state cache only if the tab has fully finished
// restoring. We don't want to overwrite data contained in __SS_data.
- this.copyFromCache(browser.permanentKey, tabData, options);
+ this.copyFromCache(tab.permanentKey, tabData, options);
// After copyFromCache() was called we check for properties that are kept
// in the cache only while the tab is pending or restoring. Once that

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -121,7 +121,15 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
on_click(event) {
if (event.button != 0) {
return;
@@ -587,6 +611,14 @@
@@ -575,6 +599,7 @@
)
);
} else {
+ gZenPinnedTabManager._removePinnedAttributes(this, true);
gBrowser.removeTab(this, {
animate: true,
triggeringEvent: event,
@@ -587,6 +612,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -136,7 +144,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
on_dblclick(event) {
@@ -610,6 +642,8 @@
@@ -610,6 +643,8 @@
animate: true,
triggeringEvent: event,
});

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18e829ca07 100644
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f53bc059b 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
const browsers = [];
if (this.#activeSplitView) {
for (const tab of this.#activeSplitView.tabs) {
@@ -450,15 +451,66 @@
@@ -450,15 +451,64 @@
return this.tabContainer.visibleTabs;
}
@@ -18,8 +18,6 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
+ return this.#handleTabMove(...args);
+ }
+
+ get zenTabProgressListener() { return TabProgressListener; }
+
+ get _numVisiblePinTabsWithoutCollapsed() {
+ let i = 0;
+ for (let item of this.tabContainer.ariaFocusableItems) {
@@ -79,7 +77,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
@@ -613,6 +665,7 @@
@@ -613,6 +663,7 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -87,7 +85,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
@@ -898,13 +951,17 @@
@@ -898,13 +949,17 @@
}
this.showTab(aTab);
@@ -106,7 +104,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
@@ -917,11 +974,15 @@
@@ -917,11 +972,15 @@
}
this.#handleTabMove(aTab, () => {
@@ -123,7 +121,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
aTab.style.marginInlineStart = "";
@@ -1098,6 +1159,8 @@
@@ -1098,6 +1157,8 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -132,7 +130,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1107,6 +1170,9 @@
@@ -1107,6 +1168,9 @@
);
return;
}
@@ -142,7 +140,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
@@ -1379,7 +1445,6 @@
@@ -1379,7 +1443,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -150,7 +148,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1470,6 +1535,7 @@
@@ -1470,6 +1533,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -158,7 +156,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -1622,6 +1688,9 @@
@@ -1622,6 +1686,9 @@
}
let activeEl = document.activeElement;
@@ -168,20 +166,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
@@ -1945,7 +2014,11 @@
@@ -1945,7 +2012,8 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
- if (!aLabel || aLabel.includes("about:reader?")) {
+ if (!aTab._zenContentsVisible && !aTab._zenChangeLabelFlag && !aTab._labelIsInitialTitle && !gZenWorkspaces.privateWindowOrDisabled) {
+ return false;
+ }
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
+ if (!aLabel || aLabel.includes("about:reader?") || (aTab.hasAttribute("zen-has-static-label") && !aTab._zenChangeLabelFlag)) {
+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) {
return false;
}
@@ -2053,7 +2126,7 @@
@@ -2053,7 +2121,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -190,7 +185,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
@@ -2328,6 +2401,7 @@
@@ -2328,6 +2396,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -198,7 +193,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
@@ -2401,8 +2475,7 @@
@@ -2401,8 +2470,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -208,7 +203,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
b.setAttribute("transparent", "true");
}
@@ -2567,7 +2640,7 @@
@@ -2567,7 +2635,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -217,7 +212,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary.
@@ -2626,8 +2699,8 @@
@@ -2626,8 +2694,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -228,7 +223,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
@@ -2814,7 +2887,6 @@
@@ -2814,7 +2882,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -236,17 +231,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
focusUrlBar: true,
});
resolve(this.selectedBrowser);
@@ -2923,6 +2995,9 @@
@@ -2923,6 +2990,8 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
+ _forZenEmptyTab,
+ essential,
+ zenWorkspaceId,
} = {}
) {
// all callers of addTab that pass a params object need to pass
@@ -2933,10 +3008,17 @@
@@ -2933,10 +3002,17 @@
);
}
@@ -264,7 +258,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
@@ -2944,6 +3026,7 @@
@@ -2944,6 +3020,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -272,16 +266,14 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
@@ -2996,6 +3079,21 @@
@@ -2996,6 +3073,19 @@
noInitialLabel,
skipBackgroundNotify,
});
+ if (hasZenDefaultUserContextId) {
+ t.setAttribute("zenDefaultUserContextId", "true");
+ }
+ if (zenWorkspaceId) {
+ t.setAttribute("zen-workspace-id", zenWorkspaceId);
+ } else if (zenForcedWorkspaceId !== undefined) {
+ if (zenForcedWorkspaceId !== undefined) {
+ t.setAttribute("zen-workspace-id", zenForcedWorkspaceId);
+ t.setAttribute("change-workspace", "")
+ }
@@ -294,7 +286,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
@@ -3004,6 +3102,7 @@
@@ -3004,6 +3094,7 @@
ownerTab,
openerTab,
pinned,
@@ -302,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
@@ -3022,6 +3121,7 @@
@@ -3022,6 +3113,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -310,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}));
if (focusUrlBar) {
@@ -3146,6 +3246,12 @@
@@ -3146,6 +3238,12 @@
}
}
@@ -323,7 +315,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
@@ -3349,10 +3455,10 @@
@@ -3349,10 +3447,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -335,7 +327,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
if (!color) {
@@ -3373,9 +3479,14 @@
@@ -3373,9 +3471,14 @@
label,
isAdoptingGroup
);
@@ -352,7 +344,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
group.addTabs(tabs);
@@ -3496,7 +3607,7 @@
@@ -3496,7 +3599,7 @@
}
this.#handleTabMove(tab, () =>
@@ -361,7 +353,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
}
@@ -3698,6 +3809,7 @@
@@ -3698,6 +3801,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -369,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -3767,6 +3879,7 @@
@@ -3767,6 +3871,7 @@
openWindowInfo,
name,
skipLoad,
@@ -377,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
}
@@ -3955,7 +4068,7 @@
@@ -3955,7 +4060,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -386,7 +378,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let url = "about:blank";
if (tabData.entries?.length) {
@@ -3992,8 +4105,10 @@
@@ -3992,8 +4097,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -398,7 +390,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (select) {
tabToSelect = tab;
}
@@ -4005,7 +4120,8 @@
@@ -4005,7 +4112,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -408,7 +400,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -4019,7 +4135,10 @@
@@ -4019,7 +4127,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -420,7 +412,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4064,9 +4183,23 @@
@@ -4064,9 +4175,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -436,15 +428,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
}
+ }
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
+ }
}
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4257,11 +4390,14 @@
@@ -4257,11 +4382,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -460,7 +452,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4273,7 +4409,7 @@
@@ -4273,7 +4401,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -469,7 +461,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
tabGroup = previousTab.group;
}
if (
@@ -4284,7 +4420,7 @@
@@ -4284,7 +4412,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -478,7 +470,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4312,14 +4448,14 @@
@@ -4312,14 +4440,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -497,7 +489,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4330,7 +4466,7 @@
@@ -4330,7 +4458,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -506,7 +498,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
@@ -4358,7 +4494,11 @@
@@ -4358,7 +4486,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -518,7 +510,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
this._updateTabsAfterInsert();
@@ -4366,6 +4506,7 @@
@@ -4366,6 +4498,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -526,7 +518,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
TabBarVisibility.update();
}
@@ -4916,6 +5057,7 @@
@@ -4655,6 +4788,9 @@
return;
}
+ for (let tab of selectedTabs) {
+ gZenPinnedTabManager._removePinnedAttributes(tab, true);
+ }
this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
}
@@ -4916,6 +5052,7 @@
telemetrySource,
} = {}
) {
@@ -534,7 +536,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5005,6 +5147,7 @@
@@ -5005,6 +5142,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -542,7 +544,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} catch (e) {
console.error(e);
}
@@ -5043,6 +5186,12 @@
@@ -5043,6 +5181,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -555,7 +557,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
@@ -5057,6 +5206,9 @@
@@ -5057,6 +5201,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -565,7 +567,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5105,7 +5257,13 @@
@@ -5105,7 +5252,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -580,7 +582,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
return;
}
@@ -5239,7 +5397,7 @@
@@ -5239,7 +5392,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -589,7 +591,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5263,6 +5421,7 @@
@@ -5263,6 +5416,7 @@
newTab = true;
}
@@ -597,7 +599,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5303,13 +5462,7 @@
@@ -5303,13 +5457,7 @@
aTab._mouseleave();
if (newTab) {
@@ -612,7 +614,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
TabBarVisibility.update();
}
@@ -5442,6 +5595,7 @@
@@ -5442,6 +5590,7 @@
this.tabs[i]._tPos = i;
}
@@ -620,7 +622,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -5663,6 +5817,7 @@
@@ -5663,6 +5812,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -628,7 +630,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -5675,13 +5830,13 @@
@@ -5675,13 +5825,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -644,7 +646,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
let tab = this.tabContainer.findNextTab(aTab, {
@@ -5697,7 +5852,7 @@
@@ -5697,7 +5847,7 @@
}
if (tab) {
@@ -653,7 +655,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -5718,7 +5873,7 @@
@@ -5718,7 +5868,7 @@
});
}
@@ -662,47 +664,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
_blurTab(aTab) {
@@ -5729,7 +5884,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
- swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
+ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) {
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -5783,6 +5938,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
+ zenCloseOther &&
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -5794,7 +5950,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
- let [closeWindow] = aOtherTab._endRemoveArgs;
+ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs;
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
@@ -5918,11 +6074,13 @@
}
// Finish tearing down the tab that's going away.
+ if (zenCloseOther) {
if (closeWindow) {
aOtherTab.ownerGlobal.close();
} else {
remoteBrowser._endRemoveTab(aOtherTab);
}
+ }
this.setTabTitle(aOurTab);
@@ -6124,10 +6282,10 @@
@@ -6124,10 +6274,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -715,33 +677,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6185,7 +6343,8 @@
*
@@ -6186,6 +6336,7 @@
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
- replaceTabWithWindow(aTab, aOptions) {
+ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) {
replaceTabWithWindow(aTab, aOptions) {
+ if (!this.isTab(aTab)) return; // TODO: Handle tab groups
if (this.tabs.length == 1) {
return null;
}
@@ -6209,12 +6368,14 @@
}
// tell a new window to take the "dropped" tab
- return window.openDialog(
+ let win = window.openDialog(
AppConstants.BROWSER_CHROME_URL,
"_blank",
options,
aTab
);
+ win._zenStartupSyncFlag = zenForceSync ? 'synced' : 'unsynced';
+ return win;
}
/**
@@ -6319,7 +6480,7 @@
@@ -6319,7 +6470,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -750,7 +694,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
/**
@@ -6404,8 +6565,8 @@
@@ -6404,8 +6555,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -761,7 +705,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -6431,10 +6592,16 @@
@@ -6431,10 +6582,16 @@
this.#handleTabMove(
element,
() => {
@@ -780,7 +724,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
@@ -6492,23 +6659,28 @@
@@ -6492,23 +6649,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -815,7 +759,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
@@ -6521,14 +6693,34 @@
@@ -6521,14 +6683,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -851,7 +795,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -6537,7 +6729,7 @@
@@ -6537,7 +6719,7 @@
element,
() => {
if (moveBefore) {
@@ -860,7 +804,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (targetElement) {
targetElement.after(element);
} else {
@@ -6607,10 +6799,10 @@
@@ -6607,10 +6789,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -873,7 +817,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
@@ -6656,6 +6848,7 @@
@@ -6656,6 +6838,7 @@
let state = {
tabIndex: tab._tPos,
@@ -881,7 +825,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -6682,7 +6875,7 @@
@@ -6682,7 +6865,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -890,7 +834,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -6723,6 +6916,10 @@
@@ -6723,6 +6906,10 @@
moveActionCallback();
@@ -901,7 +845,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7623,7 +7820,7 @@
@@ -7623,7 +7810,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -910,7 +854,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
window.focus();
aEvent.preventDefault();
break;
@@ -7640,7 +7837,6 @@
@@ -7640,7 +7827,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -918,7 +862,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
break;
case "TabGroupCreateByUser":
@@ -8589,6 +8785,7 @@
@@ -8589,6 +8775,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -926,7 +870,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9623,7 +9820,7 @@ var TabContextMenu = {
@@ -9623,7 +9810,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -935,3 +879,11 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
@@ -9943,6 +10130,7 @@ var TabContextMenu = {
)
);
} else {
+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true);
gBrowser.removeTab(this.contextTab, {
animate: true,
...gBrowser.TabMetrics.userTriggeredContext(

View File

@@ -0,0 +1,44 @@
diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
index 4db61038e5e476bad3a61dbdb707e5222c1f08f8..9eca13d9cfac3b762917aaaa942267effb743cf7 100644
--- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
@@ -45,11 +45,13 @@ function defaultQuery(conditions = "") {
let query = `
SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.id, t.open_count,
${lazy.PAGES_FRECENCY_FIELD} AS frecency, t.userContextId,
- h.last_visit_date, NULLIF(t.groupId, '') groupId
+ h.last_visit_date, NULLIF(t.groupId, '') groupId, zp.url AS pinned_url, zp.title AS pinned_title
FROM moz_places h
LEFT JOIN moz_openpages_temp t
ON t.url = h.url
AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL))
+ LEFT JOIN zen_pins zp
+ ON zp.url = h.url
WHERE (
(:switchTabsEnabled AND t.open_count > 0) OR
${lazy.PAGES_FRECENCY_FIELD} <> 0
@@ -63,7 +65,7 @@ function defaultQuery(conditions = "") {
:matchBehavior, :searchBehavior, NULL)
ELSE
AUTOCOMPLETE_MATCH(:searchString, h.url,
- h.title, '',
+ IFNULL(zp.title, h.title), '',
h.visit_count, h.typed,
0, t.open_count,
:matchBehavior, :searchBehavior, NULL)
@@ -1176,11 +1178,13 @@ class Search {
? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime()
: undefined;
let tabGroup = row.getResultByName("groupId");
+ let pinnedTitle = row.getResultByIndex(12);
+ let pinnedUrl = row.getResultByIndex("pinned_url");
let match = {
placeId,
- value: url,
- comment: bookmarkTitle || historyTitle,
+ value: pinnedUrl || url,
+ comment: pinnedTitle || bookmarkTitle || historyTitle,
icon: UrlbarUtils.getIconForUrl(url),
frecency: frecency || FRECENCY_DEFAULT,
userContextId,

View File

@@ -13,4 +13,3 @@
category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
#include common/Components.manifest
#include sessionstore/SessionComponents.manifest

View File

@@ -38,10 +38,6 @@ export class nsZenMultiWindowFeature {
if (!nsZenMultiWindowFeature.isActiveWindow) {
return;
}
return this.forEachWindow(callback);
}
async forEachWindow(callback) {
for (const browser of nsZenMultiWindowFeature.browsers) {
try {
if (browser.closed) continue;

View File

@@ -17,9 +17,8 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenWorkspace) {
tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
}
// Keep for now, for backward compatibility for window sync to work.
if (tabData.zenSyncId || tabData.zenPinnedId) {
tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId);
if (tabData.zenPinnedId) {
tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
}
if (tabData.zenHasStaticLabel) {
tab.setAttribute('zen-has-static-label', 'true');
@@ -33,9 +32,6 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenPinnedEntry) {
tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry);
}
if (tabData._zenPinnedInitialState) {
tab._zenPinnedInitialState = tabData._zenPinnedInitialState;
}
}
async #waitAndCleanup() {

View File

@@ -811,6 +811,7 @@ window.gZenVerticalTabsManager = {
!aItem.isConnected ||
gZenUIManager.testingEnabled ||
!gZenStartup.isReady ||
!gZenPinnedTabManager.hasInitializedPins ||
aItem.group?.hasAttribute('split-view-group')
) {
return;
@@ -1309,6 +1310,14 @@ window.gZenVerticalTabsManager = {
} else {
gBrowser.setTabTitle(this._tabEdited);
}
if (this._tabEdited.getAttribute('zen-pin-id')) {
// Update pin title in storage
await gZenPinnedTabManager.updatePinTitle(
this._tabEdited,
this._tabEdited.label,
!!newName
);
}
// Maybe add some confetti here?!?
gZenUIManager.motion.animate(

View File

@@ -60,15 +60,3 @@
}
}
}
.zen-pseudo-browser-image {
position: absolute;
inset: 0;
opacity: 0.4;
pointer-events: none;
z-index: 2;
}
browser[zen-pseudo-hidden='true'] {
-moz-subtree-hidden-only-visually: 1 !important;
}

View File

@@ -231,13 +231,6 @@
--toolbox-textcolor: currentColor !important;
}
&[zen-unsynced-window='true'] {
--zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%);
--zen-main-browser-background-toolbar: var(--zen-main-browser-background);
--zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important;
--toolbox-textcolor: currentColor !important;
}
--toolbar-field-background-color: var(--zen-colors-input-bg) !important;
--arrowpanel-background: var(--zen-dialog-background) !important;

View File

@@ -116,9 +116,6 @@ export const ZenCustomizableUI = new (class {
#initCreateNewButton(window) {
const button = window.document.getElementById('zen-create-new-button');
button.addEventListener('command', (event) => {
if (window.gZenWorkspaces.privateWindowOrDisabled) {
return window.document.getElementById('cmd_newNavigatorTab').doCommand();
}
if (button.hasAttribute('open')) {
return;
}

View File

@@ -131,6 +131,9 @@ class nsZenDownloadAnimationElement extends HTMLElement {
}
#areTabsOnRightSide() {
const position = Services.prefs.getIntPref('zen.downloads.icon-popup-position', 0);
if (position === 1) return false;
if (position === 2) return true;
return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
}

View File

@@ -150,6 +150,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
for (let tab of this.allItems.reverse()) {
tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab;
if (tab.hasAttribute('zen-empty-tab')) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
gBrowser.removeTab(tab);
} else {
gBrowser.ungroupTab(tab);
@@ -159,6 +160,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
async delete() {
for (const tab of this.allItemsRecursive) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
if (tab.hasAttribute('zen-empty-tab')) {
// Manually remove the empty tabs as removeTabs() inside removeTabGroup
// does ignore them.

View File

@@ -101,7 +101,20 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.querySelector('menupopup');
changeFolderSpace.innerHTML = '';
for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
const item = document.createXULElement('menuitem');
item.className = 'zen-workspace-context-menu-item';
item.setAttribute('zen-workspace-id', workspace.uuid);
item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
let name = workspace.name;
const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
name = `${workspace.icon} ${name}`;
}
item.setAttribute('label', name);
if (iconIsSvg) {
item.setAttribute('image', workspace.icon);
item.classList.add('zen-workspace-context-icon');
}
item.addEventListener('command', (event) => {
if (!this.#lastFolderContextMenu) return;
this.changeFolderToSpace(
@@ -405,7 +418,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (selectedTab) {
selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid);
selectedTab.removeAttribute('folder-active');
gZenWorkspaces.lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
}
resolve();
});
@@ -421,10 +434,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tab.style.height = '';
}
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
if (gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to
// the current workspace
delete gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid];
delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid];
}
}
}
@@ -443,9 +456,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
// we may encounter
tab.group.setAttribute('zen-workspace-id', workspaceId);
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
if (gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId] === tab) {
if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace
delete gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId];
delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId];
}
}
folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true }));
@@ -493,6 +506,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabs = [emptyTab, ...filteredTabs];
const folder = this._createFolderNode(options);
if (options.initialPinId) {
folder.setAttribute('zen-pin-id', options.initialPinId);
}
if (options.insertAfter) {
options.insertAfter.after(folder);
@@ -844,7 +860,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.open(group.icon, { onlySvgIcons: true })
.then((icon) => {
this.setFolderUserIcon(group, icon);
group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true }));
group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
})
.catch((err) => {
console.error(err);
@@ -922,7 +938,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
const emptyFolderTabs = folder.tabs
.filter((tab) => tab.hasAttribute('zen-empty-tab'))
.map((tab) => tab.getAttribute('id'));
.map((tab) => tab.getAttribute('zen-pin-id'));
let prevSiblingInfo = null;
const prevSibling = folder.previousElementSibling;
@@ -931,8 +947,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (prevSibling) {
if (gBrowser.isTabGroup(prevSibling)) {
prevSiblingInfo = { type: 'group', id: prevSibling.id };
} else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) {
prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') };
} else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
const zenPinId = prevSibling.getAttribute('zen-pin-id');
prevSiblingInfo = { type: 'tab', id: zenPinId };
} else {
prevSiblingInfo = { type: 'start', id: null };
}
@@ -950,6 +967,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
prevSiblingInfo: prevSiblingInfo,
emptyTabIds: emptyFolderTabs,
userIcon: userIcon?.getAttribute('href'),
pinId: folder.getAttribute('zen-pin-id'),
// note: We shouldn't be using the workspace-id anywhere, we are just
// remembering it for the pinned tabs manager to use it later.
workspaceId: folder.getAttribute('zen-workspace-id'),
@@ -976,8 +994,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabFolderWorkingData.set(folderData.id, workingData);
const oldGroup = document.getElementById(folderData.id);
folderData.emptyTabIds.forEach((id) => {
oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true);
folderData.emptyTabIds.forEach((zenPinId) => {
oldGroup
?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
?.setAttribute('zen-empty-tab', true);
});
if (oldGroup) {
if (!folderData.splitViewGroup) {
@@ -989,7 +1009,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
saveOnWindowClose: folderData.saveOnWindowClose,
workspaceId: folderData.workspaceId,
});
folder.setAttribute('id', folderData.id);
folder.setAttribute('zen-pin-id', folderData.pinId);
workingData.node = folder;
oldGroup.before(folder);
} else {
@@ -1021,7 +1041,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (parentWorkingData && parentWorkingData.node) {
switch (stateData?.prevSiblingInfo?.type) {
case 'tab': {
const tab = document.getElementById(stateData.prevSiblingInfo.id);
const tab = parentWorkingData.node.querySelector(
`[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
);
tab.after(node);
break;
}
@@ -1131,8 +1153,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
let firstGroupElem =
dropElementGroup?.querySelector('.zen-tab-group-start')?.nextElementSibling;
let firstGroupElem = dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling;
if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
const isInMiddleZone =

View File

@@ -13,5 +13,4 @@ DIRS += [
"tests",
"urlbar",
"toolkit",
"sessionstore",
]

View File

@@ -1,10 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Browser global components initializing before UI startup
category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init
category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
# App shutdown consumers
category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit

View File

@@ -1,298 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs';
import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs',
SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs',
setTimeout: 'resource://gre/modules/Timer.sys.mjs',
});
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true);
// Note that changing this hidden pref will make the previous session file
// unused, causing a new session file to be created on next write.
const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true);
const SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true);
const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json';
const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore';
/**
* Class representing the sidebar object stored in the session file.
* This object holds all the data related to tabs, groups, folders
* and split view state.
*/
class nsZenSidebarObject {
#sidebar = {};
get data() {
return Cu.cloneInto(this.#sidebar, {});
}
set data(data) {
this.#sidebar = data;
}
}
export class nsZenSessionManager {
/**
* The JSON file instance used to read/write session data.
* @type {JSONFile}
*/
#file = null;
/**
* The sidebar object holding tabs, groups, folders and split view data.
* @type {nsZenSidebarObject}
*/
#sidebarObject = new nsZenSidebarObject();
// Called from SessionComponents.manifest on app-startup
init() {
let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
let backupFile = null;
if (SHOULD_BACKUP_FILE) {
backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME);
}
let filePath = PathUtils.join(profileDir, FILE_NAME);
this.#file = new JSONFile({
path: filePath,
compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined,
backupFile,
});
}
log(...args) {
if (lazy.gShouldLog) {
console.info('ZenSessionManager:', ...args);
}
}
/**
* Reads the session file and populates the sidebar object.
* This should be only called once at startup.
* @see SessionFileInternal.read
*/
async readFile() {
try {
await this.#file.load();
} catch (e) {
console.error('ZenSessionManager: Failed to read session file', e);
}
this.#sidebar = this.#file.data || {};
}
/**
* Called when the session file is read. Restores the sidebar data
* into all windows.
*
* @param initialState
* The initial session state read from the session file.
*/
onFileRead(initialState) {
// For the first time after migration, we restore the tabs
// That where going to be restored by SessionStore. The sidebar
// object will always be empty after migration because we haven't
// gotten the opportunity to save the session yet.
if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
Services.prefs.setBoolPref(MIGRATION_PREF, true);
return;
}
// If there's no initial state, nothing to restore. This would
// happen if the file is empty or corrupted.
if (!initialState) {
return;
}
// If there are no windows, we create an empty one. By default,
// firefox would create simply a new empty window, but we want
// to make sure that the sidebar object is properly initialized.
// This would happen on first run after having a single private window
// open when quitting the app, for example.
if (!initialState.windows?.length) {
initialState.windows = [{}];
}
// Restore all windows with the same sidebar object, this will
// guarantee that all tabs, groups, folders and split view data
// are properly synced across all windows.
this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
for (const winData of initialState.windows || []) {
this.#restoreWindowData(winData);
}
}
get #sidebar() {
return this.#sidebarObject.data;
}
set #sidebar(data) {
this.#sidebarObject.data = data;
}
/**
* Saves the current session state. Collects data and writes to disk.
*
* @param state
* The current session state.
*/
saveState(state) {
if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) {
// Don't save (or even collect) anything in permanent private
// browsing mode. We also don't want to save if there are no windows.
return;
}
this.#collectWindowData(state);
// This would save the data to disk asynchronously or when
// quitting the app.
this.#file.data = this.#sidebar;
this.#file.saveSoon();
this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
}
/**
* Collects session data for a given window.
*
* @param state
* The current session state.
*/
#collectWindowData(state) {
let sidebarData = this.#sidebar;
if (!sidebarData) {
sidebarData = {};
}
sidebarData.lastCollected = Date.now();
this.#collectTabsData(sidebarData, state);
this.#sidebar = sidebarData;
}
#filterUnusedTabs(tabs) {
return tabs.filter((tab) => {
// We need to ignore empty tabs with no group association
// as they are not useful to restore.
return !(tab.zenIsEmpty && !tab.groupId);
});
}
/**
* Collects session data for all tabs in a given window.
*
* @param sidebarData
* The sidebar data object to populate.
* @param state
* The current session state.
*/
#collectTabsData(sidebarData, state) {
const tabIdRelationMap = new Map();
for (const window of state.windows) {
// Only accept the tabs with `_zenIsActiveTab` set to true from
// every window. We do this to avoid collecting tabs with invalid
// state when multiple windows are open. Note that if we a tab without
// this flag set in any other window, we just add it anyway.
for (const tabData of window.tabs) {
if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) {
tabIdRelationMap.set(tabData.zenSyncId, tabData);
}
}
}
sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values()));
sidebarData.folders = state.windows[0].folders;
sidebarData.splitViewData = state.windows[0].splitViewData;
sidebarData.groups = state.windows[0].groups;
}
/**
* Restores the sidebar data into a given window data object.
* We do this in order to make sure all new window objects
* have the same sidebar data.
*
* @param aWindowData
* The window data object to restore into.
*/
#restoreWindowData(aWindowData) {
const sidebar = this.#sidebar;
if (!sidebar) {
return;
}
aWindowData.tabs = sidebar.tabs || [];
aWindowData.splitViewData = sidebar.splitViewData;
aWindowData.folders = sidebar.folders;
aWindowData.groups = sidebar.groups;
}
/**
* Restores a new window with Zen session data. This should be called
* not at startup, but when a new window is opened by the user.
*
* @param aWindow
* The window to restore.
* @param SessionStoreInternal
* The SessionStore module instance.
* @param resolvePromise
* The promise resolver to call when done. We use a promise
* here because out workspace manager always waits for SessionStore
* to restore all the windows before initializing, but when opening
* a new window, that promise is always resolved, meaning it may run
* into a race condition if we try to restore the window synchronously
* here.
*/
restoreNewWindow(aWindow, SessionStoreInternal, resolvePromise) {
if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
return resolvePromise();
}
this.log('Restoring new window with Zen session data');
const state = lazy.SessionStore.getCurrentState(true);
const windows = (state.windows || []).filter(
(win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced
);
let windowToClone = windows[0] || {};
let newWindow = Cu.cloneInto(windowToClone, {});
if (windows.length < 2) {
// We only want to restore the sidebar object if we found
// only one normal window to clone from (which is the one
// we are opening).
this.log('Restoring sidebar data into new window');
this.#restoreWindowData(newWindow);
}
newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []);
// These are window-specific from the previous window state that
// we don't want to restore into the new window. Otherwise, new
// windows would appear overlapping the previous one, or with
// the same size and position, which should be decided by the
// window manager.
delete newWindow.selected;
delete newWindow.screenX;
delete newWindow.screenY;
delete newWindow.width;
delete newWindow.height;
delete newWindow.sizemode;
delete newWindow.sizemodeBeforeMinimized;
delete newWindow.zIndex;
const newState = { windows: [newWindow] };
this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
aWindow.addEventListener(
'SSWindowRestored',
() => {
lazy.setTimeout(resolvePromise);
},
{ once: true }
);
SessionStoreInternal._deferredInitialState = newState;
SessionStoreInternal.initializeWindow(aWindow, newState);
}
}
export const ZenSessionStore = new nsZenSessionManager();

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen += [
"ZenSessionManager.sys.mjs",
"ZenWindowSync.sys.mjs",
]

View File

@@ -202,7 +202,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.resetTabState(tab, forUnsplit);
if (tab.group && tab.group.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
if (group.tabs.length < 2) {
// We need to remove all remaining tabs from the group when unsplitting
@@ -896,21 +895,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
}
/**
* Dispatches a custom event on a tab.
*
* @param {string} eventName - The name of the event to dispatch.
* @param {HTMLElement} item - The item on which to dispatch the event.
*/
#dispatchItemEvent(eventName, item) {
const event = new CustomEvent(eventName, {
detail: { item },
bubbles: true,
cancelable: false,
});
item.dispatchEvent(event);
}
/**
* Removes a group.
*
@@ -921,7 +905,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
for (const tab of group.tabs.reverse()) {
if (tab.group?.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
}
if (this.currentView === groupIndex) {
@@ -1084,13 +1067,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
*
* @param {Tab[]} tabs - The tabs to split.
* @param {string|undefined} gridType - The type of grid layout.
* @param {number} initialIndex - The index of the initially active tab.
* use -1 to avoid selecting any tab.
* @return {object|undefined} The split view data or undefined if the split was not performed.
*/
splitTabs(tabs, gridType, initialIndex = 0) {
const tabIndexToUse = Math.max(0, initialIndex);
return this.#withoutSplitViewTransition(() => {
this.#withoutSplitViewTransition(() => {
// TODO: Add support for splitting essential tabs
tabs = tabs.filter((t) => !t.hidden && !t.hasAttribute('zen-empty-tab'));
if (tabs.length < 2 || tabs.length > this.MAX_TABS) {
@@ -1099,7 +1078,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const existingSplitTab = tabs.find((tab) => tab.splitView);
if (existingSplitTab) {
this._moveTabsToContainer(tabs, tabs[tabIndexToUse]);
this._moveTabsToContainer(tabs, tabs[initialIndex]);
const groupIndex = this._data.findIndex((group) => group.tabs.includes(existingSplitTab));
const group = this._data[groupIndex];
const gridTypeChange = gridType && group.gridType !== gridType;
@@ -1126,8 +1105,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(group, true);
this.#dispatchItemEvent('ZenSplitViewTabsSplit', group);
return group;
return;
}
// We are here if none of the tabs have been previously split
@@ -1154,8 +1132,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
layoutTree: this.calculateLayoutTree(tabs, gridType),
};
this._data.push(splitData);
if (!this._sessionRestoring && initialIndex >= 0) {
window.gBrowser.selectedTab = tabs[tabIndexToUse] ?? tabs[0];
if (!this._sessionRestoring) {
window.gBrowser.selectedTab = tabs[initialIndex] ?? tabs[0];
}
// Add tabs to the split view group
@@ -1172,8 +1150,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(splitData);
this.#dispatchItemEvent('ZenSplitViewTabsSplit', splitGroup);
return splitData;
});
}
@@ -1879,10 +1855,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
// We can't create an empty group, so only create if we have tabs
let group = null;
if (tabs?.length) {
// Create a new group with the initial tabs
group = gBrowser.addTabGroup(tabs, {
gBrowser.addTabGroup(tabs, {
label: '',
showCreateUI: false,
insertBefore: tabs[0],
@@ -1890,7 +1865,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
});
}
return group;
return null;
}
storeDataForSessionStore() {
@@ -1961,7 +1936,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
#withoutSplitViewTransition(callback) {
this.tabBrowserPanel.classList.add('zen-split-view-no-transition');
try {
return callback();
callback();
} finally {
requestAnimationFrame(() => {
this.tabBrowserPanel.classList.remove('zen-split-view-no-transition');

View File

@@ -7,7 +7,23 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components
const lazy = {};
class ZenPinnedTabsObserver {
static ALL_EVENTS = ['TabPinned', 'TabUnpinned'];
static ALL_EVENTS = [
'TabPinned',
'TabUnpinned',
'TabMove',
'TabGroupCreate',
'TabGroupRemoved',
'TabGroupMoved',
'ZenFolderRenamed',
'ZenFolderIconChanged',
'TabGroupCollapse',
'TabGroupExpand',
'TabGrouped',
'TabUngrouped',
'ZenFolderChangedWorkspace',
'TabAddedToEssentials',
'TabRemovedFromEssentials',
];
#listeners = [];
@@ -60,6 +76,7 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
hasInitializedPins = false;
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
@@ -86,16 +103,43 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
onTabIconChanged(tab, url = null) {
tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
const iconUrl = url ?? tab.iconImage.src;
if (tab.hasAttribute('zen-essential')) {
tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
try {
setTimeout(async () => {
const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
if (favicon) {
gBrowser.setIcon(tab, favicon);
}
});
} catch {
// Handle error
}
} else {
if (tab.hasAttribute('zen-essential')) {
tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
}
}
}
_onTabResetPinButton(event, tab) {
event.stopPropagation();
this._resetTabToStoredState(tab);
const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
let userContextId;
if (tab.hasAttribute('usercontextid')) {
userContextId = tab.getAttribute('usercontextid');
}
const pinnedUrl = Services.io.newURI(pin.url);
const browser = tab.linkedBrowser;
browser.loadURI(pinnedUrl, {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
userContextId,
}),
});
this.resetPinChangedUrl(tab);
}
get enabled() {
@@ -106,6 +150,260 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return lazy.zenTabsEssentialsMax;
}
async refreshPinnedTabs({ init = false } = {}) {
if (!this.enabled) {
return;
}
await ZenPinnedTabsStorage.promiseInitialized;
await this.#initializePinsCache();
setTimeout(async () => {
// Execute in a separate task to avoid blocking the main thread
await SessionStore.promiseAllWindowsRestored;
await gZenWorkspaces.promiseInitialized;
await this.#initializePinnedTabs(init);
if (init) {
this._hasFinishedLoading = true;
}
}, 100);
}
async #initializePinsCache() {
try {
// Get pin data
const pins = await ZenPinnedTabsStorage.getPins();
// Enhance pins with favicons
this._pinsCache = await Promise.all(
pins.map(async (pin) => {
try {
if (pin.isGroup) {
return pin; // Skip groups for now
}
const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
return {
...pin,
iconUrl: image || null,
};
} catch {
// If favicon fetch fails, continue without icon
return {
...pin,
iconUrl: null,
};
}
})
);
} catch (ex) {
console.error('Failed to initialize pins cache:', ex);
this._pinsCache = [];
}
this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
return this._pinsCache;
}
#finishedInitializingPins() {
if (this.hasInitializedPins) {
return;
}
this._resolvePinnedInitializedInternal();
delete this._resolvePinnedInitializedInternal;
this.hasInitializedPins = true;
}
async #initializePinnedTabs(init = false) {
const pins = this._pinsCache;
if (!pins?.length || !init) {
this.#finishedInitializingPins();
return;
}
const pinnedTabsByUUID = new Map();
const pinsToCreate = new Set(pins.map((p) => p.uuid));
// First pass: identify existing tabs and remove those without pins
for (let tab of gZenWorkspaces.allStoredTabs) {
const pinId = tab.getAttribute('zen-pin-id');
if (!pinId) {
continue;
}
if (pinsToCreate.has(pinId)) {
// This is a valid pinned tab that matches a pin
pinnedTabsByUUID.set(pinId, tab);
pinsToCreate.delete(pinId);
if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) {
this._resetTabToStoredState(tab);
}
} else {
// This is a pinned tab that no longer has a corresponding pin
gBrowser.removeTab(tab);
}
}
for (const group of gZenWorkspaces.allTabGroups) {
const pinId = group.getAttribute('zen-pin-id');
if (!pinId) {
continue;
}
if (pinsToCreate.has(pinId)) {
// This is a valid pinned group that matches a pin
pinsToCreate.delete(pinId);
}
}
// Second pass: For every existing tab, update its label
// and set 'zen-has-static-label' attribute if it's been edited
for (let pin of pins) {
const tab = pinnedTabsByUUID.get(pin.uuid);
if (!tab) {
continue;
}
tab.removeAttribute('zen-has-static-label'); // So we can set it again
if (pin.title && pin.editedTitle) {
gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
tab.setAttribute('zen-has-static-label', 'true');
}
}
const groups = new Map();
const pendingTabsInsideGroups = {};
// Third pass: create new tabs for pins that don't have tabs
for (let pin of pins) {
try {
if (!pinsToCreate.has(pin.uuid)) {
continue; // Skip pins that already have tabs
}
if (pin.isGroup) {
const tabs = [];
// If there's already existing tabs, let's use them
for (const [uuid, existingTab] of pinnedTabsByUUID) {
const pinObject = this._pinsCache.find((p) => p.uuid === uuid);
if (pinObject && pinObject.parentUuid === pin.uuid) {
tabs.push(existingTab);
}
}
// We still need to iterate through pending tabs since the database
// query doesn't guarantee the order of insertion
for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) {
if (parentUuid === pin.uuid) {
tabs.push(...folderTabs);
}
}
const group = gZenFolders.createFolder(tabs, {
label: pin.title,
collapsed: pin.isFolderCollapsed,
initialPinId: pin.uuid,
workspaceId: pin.workspaceUuid,
insertAfter:
groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null,
});
gZenFolders.setFolderUserIcon(group, pin.folderIcon);
groups.set(pin.uuid, group);
continue;
}
let params = {
skipAnimation: true,
allowInheritPrincipal: false,
skipBackgroundNotify: true,
userContextId: pin.containerTabId || 0,
createLazyBrowser: true,
skipLoad: true,
noInitialLabel: false,
};
// Create and initialize the tab
let newTab = gBrowser.addTrustedTab(pin.url, params);
newTab.setAttribute('zenDefaultUserContextId', true);
// Set initial label/title
if (pin.title) {
gBrowser.setInitialTabTitle(newTab, pin.title);
}
// Set the icon if we have it cached
if (pin.iconUrl) {
gBrowser.setIcon(newTab, pin.iconUrl);
}
newTab.setAttribute('zen-pin-id', pin.uuid);
if (pin.workspaceUuid) {
newTab.setAttribute('zen-workspace-id', pin.workspaceUuid);
}
if (pin.isEssential) {
newTab.setAttribute('zen-essential', 'true');
}
if (pin.editedTitle) {
newTab.setAttribute('zen-has-static-label', 'true');
}
// Initialize browser state if needed
if (!newTab.linkedBrowser._remoteAutoRemoved) {
let state = {
entries: [
{
url: pin.url,
title: pin.title,
triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
},
],
userContextId: pin.containerTabId || 0,
image: pin.iconUrl,
};
SessionStore.setTabState(newTab, state);
}
this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`);
gBrowser.pinTab(newTab);
if (pin.parentUuid) {
const parentGroup = groups.get(pin.parentUuid);
if (parentGroup) {
parentGroup.querySelector('.tab-group-container').appendChild(newTab);
} else {
if (pendingTabsInsideGroups[pin.parentUuid]) {
pendingTabsInsideGroups[pin.parentUuid].push(newTab);
} else {
pendingTabsInsideGroups[pin.parentUuid] = [newTab];
}
}
} else {
if (!pin.isEssential) {
const container = gZenWorkspaces.workspaceElement(
pin.workspaceUuid
)?.pinnedTabsContainer;
if (container) {
container.insertBefore(newTab, container.lastChild);
}
} else {
gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab);
}
}
gBrowser.tabContainer._invalidateCachedTabs();
newTab.initialize();
} catch (ex) {
console.error('Failed to initialize pinned tabs:', ex);
}
}
setTimeout(() => {
this.#finishedInitializingPins();
}, 0);
gBrowser._updateTabBarForPinnedTabs();
gZenUIManager.updateTabsToolbar();
}
_onPinnedTabEvent(action, event) {
if (!this.enabled) return;
const tab = event.target;
@@ -115,24 +413,236 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
switch (action) {
case 'TabPinned':
case 'TabAddedToEssentials':
tab._zenClickEventListener = this._zenClickEventListener;
tab.addEventListener('click', tab._zenClickEventListener);
this._setPinnedAttributes(tab);
break;
case 'TabRemovedFromEssentials':
if (tab.pinned) {
this.#onTabMove(tab);
break;
}
// [Fall through]
case 'TabUnpinned':
this._removePinnedAttributes(tab);
if (tab._zenClickEventListener) {
tab.removeEventListener('click', tab._zenClickEventListener);
delete tab._zenClickEventListener;
}
break;
case 'TabMove':
this.#onTabMove(tab);
break;
case 'TabGroupCreate':
this.#onTabGroupCreate(event);
break;
case 'TabGroupRemoved':
this.#onTabGroupRemoved(event);
break;
case 'TabGroupMoved':
this.#onTabGroupMoved(event);
break;
case 'ZenFolderRenamed':
case 'ZenFolderIconChanged':
case 'TabGroupCollapse':
case 'TabGroupExpand':
case 'ZenFolderChangedWorkspace':
this.#updateGroupInfo(event.originalTarget, action);
break;
case 'TabGrouped':
this.#onTabGrouped(event);
break;
case 'TabUngrouped':
this.#onTabUngrouped(event);
break;
default:
console.warn('ZenPinnedTabManager: Unhandled tab event', action);
break;
}
}
#getTabState(tab) {
return JSON.parse(SessionStore.getTabState(tab));
async #onTabGroupCreate(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
if (group.hasAttribute('zen-pin-id')) {
return; // Group already exists in storage
}
const workspaceId = group.getAttribute('zen-workspace-id');
let id = await ZenPinnedTabsStorage.createGroup(
group.name,
group.iconURL,
group.collapsed,
workspaceId,
group.getAttribute('zen-pin-id'),
group._pPos
);
group.setAttribute('zen-pin-id', id);
for (const tab of group.tabs) {
// Only add it if the tab is directly under the group
if (
tab.pinned &&
tab.hasAttribute('zen-pin-id') &&
tab.group === group &&
this.hasInitializedPins
) {
const tabPinId = tab.getAttribute('zen-pin-id');
await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
}
}
await this.refreshPinnedTabs();
}
async #onTabGrouped(event) {
const tab = event.detail;
const group = tab.group;
if (!group.isZenFolder) {
return;
}
const pinId = group.getAttribute('zen-pin-id');
const tabPinId = tab.getAttribute('zen-pin-id');
const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
if (!tabPin || !tabPin.group) {
return;
}
ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
}
async #onTabUngrouped(event) {
const tab = event.detail;
const group = tab.group;
if (!group?.isZenFolder) {
return;
}
const tabPinId = tab.getAttribute('zen-pin-id');
const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
if (!tabPin) {
return;
}
ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
}
async #updateGroupInfo(group, action) {
if (!group?.isZenFolder) {
return;
}
const pinId = group.getAttribute('zen-pin-id');
const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
if (groupPin) {
groupPin.title = group.name;
groupPin.folderIcon = group.iconURL;
groupPin.isFolderCollapsed = group.collapsed;
groupPin.position = group._pPos;
groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
await this.savePin(groupPin);
switch (action) {
case 'ZenFolderRenamed':
case 'ZenFolderIconChanged':
case 'TabGroupCollapse':
case 'TabGroupExpand':
break;
default:
for (const item of group.allItems) {
if (gBrowser.isTabGroup(item)) {
await this.#updateGroupInfo(item, action);
} else {
await this.#onTabMove(item);
}
}
}
}
}
async #onTabGroupRemoved(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
group.removeAttribute('zen-pin-id');
}
async #onTabGroupMoved(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
const newIndex = group._pPos;
const pinId = group.getAttribute('zen-pin-id');
if (!pinId) {
return;
}
for (const tab of group.allItemsRecursive) {
if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
const pin = this._pinsCache.find((p) => p.uuid === pinId);
if (pin) {
pin.position = tab._pPos;
pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
pin.workspaceUuid = group.getAttribute('zen-workspace-id');
await this.savePin(pin, false);
}
break;
}
}
const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
if (groupPin) {
groupPin.position = newIndex;
groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
await this.savePin(groupPin);
}
}
async #onTabMove(tab) {
if (!tab.pinned || !this._pinsCache) {
return;
}
const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
for (let i = 0; i < allTabs.length; i++) {
const otherTab = allTabs[i];
if (
otherTab.pinned &&
otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
) {
const actualPin = this._pinsCache.find(
(pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
);
if (!actualPin) {
continue;
}
actualPin.position = otherTab._pPos;
actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
await this.savePin(actualPin, false);
}
}
const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!actualPin) {
return;
}
actualPin.position = tab._pPos;
actualPin.isEssential = tab.hasAttribute('zen-essential');
actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
// There was a bug where the title and hasStaticLabel attribute were not being set
// This is a workaround to fix that
if (tab.hasAttribute('zen-has-static-label')) {
actualPin.editedTitle = true;
actualPin.title = tab.label;
}
await this.savePin(actualPin);
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabMoved', {
detail: { tab },
})
);
}
async _onTabClick(e) {
@@ -158,15 +668,110 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
async replacePinnedUrlWithCurrent(tab = undefined) {
tab ??= TabContextMenu.contextTab;
if (!tab || !tab.pinned) {
if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) {
return;
}
window.gZenWindowSync.setPinnedTabState(tab);
const browser = tab.linkedBrowser;
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
const userContextId = tab.getAttribute('usercontextid');
pin.title = tab.label || browser.contentTitle;
pin.url = browser.currentURI.spec;
pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
await this.savePin(pin);
this.resetPinChangedUrl(tab);
await this.refreshPinnedTabs();
gZenUIManager.showToast('zen-pinned-tab-replaced');
}
async _setPinnedAttributes(tab) {
if (
tab.hasAttribute('zen-pin-id') ||
!this._hasFinishedLoading ||
tab.hasAttribute('zen-empty-tab')
) {
return;
}
this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
const browser = tab.linkedBrowser;
const uuid = gZenUIManager.generateUuidv4();
const userContextId = tab.getAttribute('usercontextid');
let entry = null;
if (tab.getAttribute('zen-pinned-entry')) {
entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
}
await this.savePin({
uuid,
title: entry?.title || tab.label || browser.contentTitle,
url: entry?.url || browser.currentURI.spec,
containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
workspaceUuid: tab.getAttribute('zen-workspace-id'),
isEssential: tab.getAttribute('zen-essential') === 'true',
parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
position: tab._pPos,
});
tab.setAttribute('zen-pin-id', uuid);
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabCreated', {
detail: { tab },
})
);
// This is used while migrating old pins to new system - we don't want to refresh when migrating
if (tab.getAttribute('zen-pinned-entry')) {
tab.removeAttribute('zen-pinned-entry');
return;
}
this.onLocationChange(browser);
await this.refreshPinnedTabs();
}
async _removePinnedAttributes(tab, isClosing = false) {
tab.removeAttribute('zen-has-static-label');
if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
return;
}
if (Services.startup.shuttingDown || window.skipNextCanClose) {
return;
}
this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
this.resetPinChangedUrl(tab);
if (!isClosing) {
tab.removeAttribute('zen-pin-id');
tab.removeAttribute('zen-essential'); // Just in case
if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
const workspace = await gZenWorkspaces.getActiveWorkspace();
tab.setAttribute('zen-workspace-id', workspace.uuid);
}
}
await this.refreshPinnedTabs();
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabRemoved', {
detail: { tab },
})
);
}
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById('cmd_close');
@@ -175,6 +780,21 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
async savePin(pin, notifyObservers = true) {
if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
return;
}
const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
if (existingPin) {
Object.assign(existingPin, pin);
} else {
// We shouldn't need it, but just in case there's
// a race condition while making new pinned tabs.
this._pinsCache.push(pin);
}
await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
}
async onCloseTabShortcut(
event,
selectedTab = gBrowser.selectedTab,
@@ -221,6 +841,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
switch (behavior) {
case 'close': {
for (const tab of pinnedTabs) {
this._removePinnedAttributes(tab, true);
gBrowser.removeTab(tab, { animate: true });
}
break;
@@ -322,14 +943,35 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
_resetTabToStoredState(tab) {
const state = this.#getTabState(tab);
const id = tab.getAttribute('zen-pin-id');
if (!id) {
return;
}
const initialState = tab._zenPinnedInitialState;
const pin = this._pinsCache.find((pin) => pin.uuid === id);
if (!pin) {
return;
}
// Remove everything except the entry we want to keep
state.entries = [initialState.entry];
const tabState = SessionStore.getTabState(tab);
const state = JSON.parse(tabState);
state.image = initialState.image;
const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url);
if (foundEntryIndex === -1) {
state.entries = [
{
url: pin.url,
title: pin.title,
triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
},
];
} else {
// Remove everything except the entry we want to keep
const existingEntry = state.entries[foundEntryIndex];
existingEntry.title = pin.title;
state.entries = [existingEntry];
}
state.image = pin.iconUrl || state.image;
state.index = 0;
SessionStore.setTabState(tab, state);
@@ -374,15 +1016,22 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
if (tab.hasAttribute('zen-workspace-id')) {
tab.removeAttribute('zen-workspace-id');
}
if (tab.pinned) {
if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (pin) {
pin.isEssential = true;
pin.workspaceUuid = null;
this.savePin(pin);
}
gBrowser.zenHandleTabMove(tab, () => {
if (tab.ownerGlobal !== window) {
tab = gBrowser.adoptTab(tab, {
selectTab: tab.selected,
});
tab.setAttribute('zen-essential', 'true');
} else {
section.appendChild(tab);
}
section.appendChild(tab);
});
} else {
gBrowser.pinTab(tab);
@@ -475,7 +1124,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
const isVisible = contextTab.pinned && !contextTab.multiselected;
const zenAddEssential = document.getElementById('context_zen-add-essential');
document.getElementById('context_zen-reset-pinned-tab').hidden = !isVisible;
document.getElementById('context_zen-reset-pinned-tab').hidden =
!isVisible || !contextTab.getAttribute('zen-pin-id');
document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
zenAddEssential.setAttribute(
@@ -611,25 +1261,24 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
onLocationChange(browser) {
async onLocationChange(browser) {
const tab = gBrowser.getTabForBrowser(browser);
if (
!tab ||
!tab.pinned ||
tab.hasAttribute('zen-essential') ||
!tab._zenPinnedInitialState?.entry
) {
if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) {
return;
}
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
// Remove # and ? from the URL
const pinUrl = tab._zenPinnedInitialState.entry.url.split('#')[0];
const pinUrl = pin.url.split('#')[0];
const currentUrl = browser.currentURI.spec.split('#')[0];
// Add an indicator that the pin has been changed
if (pinUrl === currentUrl) {
this.resetPinChangedUrl(tab);
return;
}
this.pinHasChangedUrl(tab);
this.pinHasChangedUrl(tab, pin);
}
resetPinChangedUrl(tab) {
@@ -641,7 +1290,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.style.removeProperty('--zen-original-tab-icon');
}
pinHasChangedUrl(tab) {
pinHasChangedUrl(tab, pin) {
if (tab.hasAttribute('zen-pinned-changed')) {
return;
}
@@ -650,7 +1299,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} else {
tab.setAttribute('zen-pinned-changed', 'true');
}
tab.style.setProperty('--zen-original-tab-icon', `url(${tab._zenPinnedInitialState.image})`);
tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`);
}
removeTabContainersDragoverClass(hideIndicator = true) {
@@ -766,13 +1415,39 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
async updatePinTitle(tab, newTitle, isEdited = true) {
tab.removeAttribute('zen-has-static-label');
if (isEdited) {
gBrowser._setTabLabel(tab, newTitle);
tab.setAttribute('zen-has-static-label', 'true');
} else {
gBrowser.setTabTitle(tab);
async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
const uuid = tab.getAttribute('zen-pin-id');
await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
await this.refreshPinnedTabs();
const browsers = Services.wm.getEnumerator('navigator:browser');
// update the label for the same pin across all windows
for (const browser of browsers) {
const tabs = browser.gBrowser.tabs;
// Fix pinned cache for the browser
const browserCache = browser.gZenPinnedTabManager?._pinsCache;
if (browserCache) {
const pin = browserCache.find((pin) => pin.uuid === uuid);
if (pin) {
pin.title = newTitle;
pin.editedTitle = isEdited;
}
}
for (let i = 0; i < tabs.length; i++) {
const tabToEdit = tabs[i];
if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {
tabToEdit.removeAttribute('zen-has-static-label');
if (isEdited) {
gBrowser._setTabLabel(tabToEdit, newTitle);
tabToEdit.setAttribute('zen-has-static-label', 'true');
} else {
gBrowser.setTabTitle(tabToEdit);
}
break;
}
}
}
}
@@ -888,8 +1563,19 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
onTabLabelChanged(tab) {
tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } }));
async onTabLabelChanged(tab) {
if (!this._pinsCache) {
return;
}
// If our current pin in the cache point to about:blank, we need to update the entry
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') {
await this.replacePinnedUrlWithCurrent(tab);
}
}
}

View File

@@ -0,0 +1,660 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
window.ZenPinnedTabsStorage = {
_saveCache: [],
async init() {
await this._ensureTable();
},
async _ensureTable() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => {
// Create the pins table if it doesn't exist
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins (
id INTEGER PRIMARY KEY,
uuid TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
url TEXT,
container_id INTEGER,
workspace_uuid TEXT,
position INTEGER NOT NULL DEFAULT 0,
is_essential BOOLEAN NOT NULL DEFAULT 0,
is_group BOOLEAN NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
const columns = await db.execute(`PRAGMA table_info(zen_pins)`);
const columnNames = columns.map((row) => row.getResultByName('name'));
// Helper function to add column if it doesn't exist
const addColumnIfNotExists = async (columnName, definition) => {
if (!columnNames.includes(columnName)) {
await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`);
}
};
await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0');
await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0');
await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL');
await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL');
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid)
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins_changes (
uuid TEXT PRIMARY KEY,
timestamp INTEGER NOT NULL
)
`);
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid)
`);
this._resolveInitialized();
});
},
/**
* Private helper method to notify observers with a list of changed UUIDs.
* @param {string} event - The observer event name.
* @param {Array<string>} uuids - Array of changed workspace UUIDs.
*/
_notifyPinsChanged(event, uuids) {
if (uuids.length === 0) return; // No changes to notify
// Convert the array of UUIDs to a JSON string
const data = JSON.stringify(uuids);
Services.obs.notifyObservers(null, event, data);
},
async savePin(pin, notifyObservers = true) {
// If we find the exact same pin in the cache, skip saving
const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid);
const copy = { ...pin };
if (existingIndex !== -1) {
const existingPin = this._saveCache[existingIndex];
const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]);
if (isSame) {
return; // No changes, skip saving
} else {
// Update the cached pin
this._saveCache[existingIndex] = { ...copy };
}
} else {
// Add to cache
this._saveCache.push(copy);
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
let newPosition;
if ('position' in pin && Number.isFinite(pin.position)) {
newPosition = pin.position;
} else {
// Get the maximum position within the same parent group (or null for root level)
const maxPositionResult = await db.execute(
`
SELECT MAX("position") as max_position
FROM zen_pins
WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '')
`,
{ folder_parent_uuid: pin.parentUuid || null }
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
// Insert or replace the pin
await db.executeCached(
`
INSERT OR REPLACE INTO zen_pins (
uuid, title, url, container_id, workspace_uuid, position,
is_essential, is_group, folder_parent_uuid, edited_title, created_at,
updated_at, is_folder_collapsed, folder_icon
) VALUES (
:uuid, :title, :url, :container_id, :workspace_uuid, :position,
:is_essential, :is_group, :folder_parent_uuid, :edited_title,
COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now),
:now, :is_folder_collapsed, :folder_icon
)
`,
{
uuid: pin.uuid,
title: pin.title,
url: pin.isGroup ? '' : pin.url,
container_id: pin.containerTabId || null,
workspace_uuid: pin.workspaceUuid || null,
position: newPosition,
is_essential: pin.isEssential || false,
is_group: pin.isGroup || false,
folder_parent_uuid: pin.parentUuid || null,
edited_title: pin.editedTitle || false,
now,
folder_icon: pin.folderIcon || null,
is_folder_collapsed: pin.isFolderCollapsed || false,
}
);
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: pin.uuid,
timestamp: Math.floor(now / 1000),
}
);
changedUUIDs.add(pin.uuid);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async getPins() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.executeCached(`
SELECT * FROM zen_pins
ORDER BY position ASC
`);
return rows.map((row) => ({
uuid: row.getResultByName('uuid'),
title: row.getResultByName('title'),
url: row.getResultByName('url'),
containerTabId: row.getResultByName('container_id'),
workspaceUuid: row.getResultByName('workspace_uuid'),
position: row.getResultByName('position'),
isEssential: Boolean(row.getResultByName('is_essential')),
isGroup: Boolean(row.getResultByName('is_group')),
parentUuid: row.getResultByName('folder_parent_uuid'),
editedTitle: Boolean(row.getResultByName('edited_title')),
folderIcon: row.getResultByName('folder_icon'),
isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')),
}));
},
/**
* Create a new group
* @param {string} title - The title of the group
* @param {string} workspaceUuid - The workspace UUID (optional)
* @param {string} parentUuid - The parent group UUID (optional, null for root level)
* @param {number} position - The position of the group (optional, will auto-calculate if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
* @returns {Promise<string>} The UUID of the created group
*/
async createGroup(
title,
icon = null,
isCollapsed = false,
workspaceUuid = null,
parentUuid = null,
position = null,
notifyObservers = true
) {
if (!title || typeof title !== 'string') {
throw new Error('Group title is required and must be a string');
}
const groupUuid = gZenUIManager.generateUuidv4();
const groupPin = {
uuid: groupUuid,
title,
folderIcon: icon || null,
isFolderCollapsed: isCollapsed || false,
workspaceUuid,
parentUuid,
position,
isGroup: true,
isEssential: false,
editedTitle: true, // Group titles are always considered edited
};
await this.savePin(groupPin, notifyObservers);
return groupUuid;
},
/**
* Add an existing tab/pin to a group
* @param {string} tabUuid - The UUID of the tab to add to the group
* @param {string} groupUuid - The UUID of the target group
* @param {number} position - The position within the group (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) {
if (!tabUuid || !groupUuid) {
throw new Error('Both tabUuid and groupUuid are required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => {
await db.executeTransaction(async () => {
// Verify the group exists and is actually a group
const groupCheck = await db.execute(
`SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`,
{ groupUuid }
);
if (groupCheck.length === 0) {
throw new Error(`Group with UUID ${groupUuid} does not exist`);
}
if (!groupCheck[0].getResultByName('is_group')) {
throw new Error(`Pin with UUID ${groupUuid} is not a group`);
}
const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, {
tabUuid,
});
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
const now = Date.now();
let newPosition;
if (position !== null && Number.isFinite(position)) {
newPosition = position;
} else {
// Get the maximum position within the group
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`,
{ groupUuid }
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = :groupUuid,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
groupUuid,
newPosition,
now,
}
);
changedUUIDs.add(tabUuid);
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
/**
* Remove a tab from its group (move to root level)
* @param {string} tabUuid - The UUID of the tab to remove from its group
* @param {number} newPosition - The new position at root level (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) {
if (!tabUuid) {
throw new Error('tabUuid is required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper(
'ZenPinnedTabsStorage.removeTabFromGroup',
async (db) => {
await db.executeTransaction(async () => {
// Verify the tab exists and is in a group
const tabCheck = await db.execute(
`SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`,
{ tabUuid }
);
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
if (!tabCheck[0].getResultByName('folder_parent_uuid')) {
return;
}
const now = Date.now();
let finalPosition;
if (newPosition !== null && Number.isFinite(newPosition)) {
finalPosition = newPosition;
} else {
// Get the maximum position at root level (where folder_parent_uuid is null)
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL`
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
finalPosition = maxPosition + 1000;
}
// Update the tab to be at root level
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = NULL,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
newPosition: finalPosition,
now,
}
);
changedUUIDs.add(tabUuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
}
);
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async removePin(uuid, notifyObservers = true) {
const cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid);
if (cachedIndex !== -1) {
this._saveCache.splice(cachedIndex, 1);
}
const changedUUIDs = [uuid];
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => {
await db.executeTransaction(async () => {
// Get all child UUIDs first for change tracking
const children = await db.execute(
`SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`,
{
uuid,
}
);
// Add child UUIDs to changedUUIDs array
for (const child of children) {
changedUUIDs.push(child.getResultByName('uuid'));
}
// Delete the pin/group itself
await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid });
// Record the changes
const now = Math.floor(Date.now() / 1000);
for (const changedUuid of changedUUIDs) {
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: changedUuid,
timestamp: now,
}
);
}
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-removed', changedUUIDs);
}
},
async wipeAllPins() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => {
await db.execute(`DELETE FROM zen_pins`);
await db.execute(`DELETE FROM zen_pins_changes`);
await this.updateLastChangeTimestamp(db);
});
},
async markChanged(uuid) {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => {
const now = Date.now();
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid,
timestamp: Math.floor(now / 1000),
}
);
});
},
async getChangedIDs() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT uuid, timestamp FROM zen_pins_changes
`);
const changes = {};
for (const row of rows) {
changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
}
return changes;
},
async clearChangedIDs() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => {
await db.execute(`DELETE FROM zen_pins_changes`);
});
},
shouldReorderPins(before, current, after) {
const minGap = 1; // Minimum allowed gap between positions
return (
(before !== null && current - before < minGap) || (after !== null && after - current < minGap)
);
},
async reorderAllPins(db, changedUUIDs) {
const pins = await db.execute(`
SELECT uuid
FROM zen_pins
ORDER BY position ASC
`);
for (let i = 0; i < pins.length; i++) {
const newPosition = (i + 1) * 1000; // Use large increments
await db.execute(
`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`,
{ newPosition, uuid: pins[i].getResultByName('uuid') }
);
changedUUIDs.add(pins[i].getResultByName('uuid'));
}
},
async updateLastChangeTimestamp(db) {
const now = Date.now();
await db.execute(
`
INSERT OR REPLACE INTO moz_meta (key, value)
VALUES ('zen_pins_last_change', :now)
`,
{ now }
);
},
async getLastChangeTimestamp() {
const db = await PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change'
`);
return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
},
async updatePinPositions(pins) {
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper(
'ZenPinnedTabsStorage.updatePinPositions',
async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
for (let i = 0; i < pins.length; i++) {
const pin = pins[i];
const newPosition = (i + 1) * 1000;
await db.execute(
`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`,
{ newPosition, uuid: pin.uuid }
);
changedUUIDs.add(pin.uuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: pin.uuid,
timestamp: Math.floor(now / 1000),
}
);
}
await this.updateLastChangeTimestamp(db);
});
}
);
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
},
async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) {
if (!uuid || typeof newTitle !== 'string') {
throw new Error('Invalid parameters: uuid and newTitle are required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
// Update the pin's title and edited_title flag
const result = await db.execute(
`
UPDATE zen_pins
SET title = :newTitle,
edited_title = :isEdited,
updated_at = :now
WHERE uuid = :uuid
`,
{
uuid,
newTitle,
isEdited,
now,
}
);
// Only proceed with change tracking if a row was actually updated
if (result.rowsAffected > 0) {
changedUUIDs.add(uuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
}
});
});
if (notifyObservers && changedUUIDs.size > 0) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async __dropTables() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => {
await db.execute(`DROP TABLE IF EXISTS zen_pins`);
await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`);
});
},
};
ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => {
ZenPinnedTabsStorage._resolveInitialized = resolve;
ZenPinnedTabsStorage.init();
});

View File

@@ -2,6 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs)
content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs)
* content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css)
content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css)

View File

@@ -31,6 +31,9 @@ add_task(async function test_Duplicate_Tab_Inside_Folder() {
for (const t of folder.tabs) {
ok(t.pinned, 'All tabs in the folder should be pinned');
if (!t.hasAttribute('zen-empty-tab')) {
ok(t.hasAttribute('zen-pin-id'), 'All non-empty tabs should have a zen-pinned-id attribute');
}
}
gBrowser.selectedTab = selectedTab;

View File

@@ -13,9 +13,14 @@ prefs = ["zen.workspaces.separate-essentials=false"]
["browser_pinned_close.js"]
["browser_pinned_changed.js"]
["browser_pinned_created.js"]
["browser_pinned_edit_label.js"]
["browser_pinned_removed.js"]
["browser_pinned_reorder_changed_label.js"]
["browser_pinned_reordered.js"]
["browser_pinned_to_essential.js"]
["browser_private_mode_no_essentials.js"]
["browser_private_mode_no_ctx_menu.js"]
["browser_issue_7654.js"]
["browser_issue_8726.js"]

View File

@@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
ChromeUtils.defineESModuleGetters(this, {
UrlbarTestUtils: 'resource://testing-common/UrlbarTestUtils.sys.mjs',
});
add_task(async function test_Search_Pinned_Title() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const customLabel = 'ZEN ROCKS';
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
tab.addEventListener(
'ZenPinnedTabCreated',
async function () {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true);
const pinnedTabs = await ZenPinnedTabsStorage.getPins();
const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID);
Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title');
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/2', true);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: customLabel,
waitForFocus: SimpleTest.waitForFocus,
});
const total = UrlbarTestUtils.getResultCount(window);
info(`Found ${total} matches`);
const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
const url = result?.url;
Assert.equal(
url,
'https://example.com/1',
`Should have the found result '${url}' in the expected list of entries`
);
Assert.equal(
result?.title,
customLabel,
`Should have the found result '${result?.title}' in the expected list of entries`
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
resolvePromise();
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -11,20 +11,27 @@ add_task(async function test_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
resolvePromise();
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
resolvePromise();
}, 0);
await promise;
});
});

View File

@@ -15,13 +15,20 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(tab.closing, 'The tab should be closing after being closed');
resolvePromise();
}, 100);
},
{ once: true }
);
gBrowser.pinTab(tab);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(tab.closing, 'The tab should be closing after being closed');
resolvePromise();
}, 100);
await promise;
});
});

View File

@@ -12,24 +12,38 @@ add_task(async function test_Create_Pinned() {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
try {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
Assert.equal(
pinObject.url,
'https://example.com/',
'The pin object should have the correct URL'
);
Assert.equal(
pinObject.workspaceUuid,
gZenWorkspaces.activeWorkspace,
'The pin object should have the correct workspace UUID'
);
} catch (error) {
ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
}
resolvePromise();
},
{ once: true }
);
gBrowser.pinTab(newTab);
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
try {
const pinObject = newTab.__zenPinnedInitialState;
ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
Assert.equal(
pinObject.entry.url,
'https://example.com/',
'The pin object should have the correct URL'
);
} catch (error) {
ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
}
resolvePromise();
await promise;
await BrowserTestUtils.removeTab(newTab);
});

View File

@@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
add_task(async function test_Create_Pinned() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const customLabel = 'Test Label';
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true);
const pinnedTabs = await ZenPinnedTabsStorage.getPins();
const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID);
Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title');
Assert.equal(
pinObject.url,
'https://example.com/',
'The pin object should have the correct URL'
);
resolvePromise();
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -15,26 +15,37 @@ add_task(async function test_NoUnload_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
gBrowser.pinTab(tab);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(
!tab.hasAttribute('discarded'),
'The tab should not be discarded after being closed'
);
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
add_task(async function test_Remove_Pinned() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
newTab.addEventListener(
'ZenPinnedTabRemoved',
async function (event) {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
ok(
!pinObject,
'The pin object should not exist in the ZenPinnedTabsStorage after removal'
);
ok(
!newTab.hasAttribute('zen-pin-id'),
'The tab should not have a zen-pin-id attribute after removal'
);
ok(!newTab.pinned, 'The tab should not be pinned after removal');
resolvePromise();
},
{ once: true }
);
gBrowser.unpinTab(newTab);
},
{ once: true }
);
gBrowser.pinTab(newTab);
await promise;
await BrowserTestUtils.removeTab(newTab);
});

View File

@@ -0,0 +1,126 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
add_task(async function test_Pinned_Reorder_Changed_Label() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const tabsToRemove = [];
for (let i = 0; i < 3; i++) {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
gBrowser.pinTab(gBrowser.selectedTab);
tabsToRemove.push(gBrowser.selectedTab);
}
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
tabsToRemove.push(gBrowser.selectedTab);
const customLabel = 'Test Label';
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true);
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
newTab.addEventListener(
'ZenPinnedTabMoved',
async function (event) {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
Assert.equal(
pinObject.title,
customLabel,
'The pin object should have the correct title'
);
Assert.equal(
pinObject.position,
2,
'The pin object should have the correct position after moving'
);
resolvePromise();
},
{ once: true }
);
gBrowser.moveTabTo(newTab, { tabIndex: 2 });
},
{ once: true }
);
gBrowser.pinTab(newTab);
await promise;
for (const tab of tabsToRemove) {
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_Pinned_Reorder_Changed_Label() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const tabsToRemove = [];
for (let i = 0; i < 3; i++) {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
gBrowser.pinTab(gBrowser.selectedTab);
tabsToRemove.push(gBrowser.selectedTab);
}
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
tabsToRemove.push(gBrowser.selectedTab);
const customLabel = 'Test Label';
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
newTab.addEventListener(
'ZenPinnedTabMoved',
async function (event) {
await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true);
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
Assert.equal(
pinObject.title,
customLabel,
'The pin object should have the correct title'
);
Assert.equal(
pinObject.position,
1,
'The pin object should have the correct position after moving'
);
resolvePromise();
},
{ once: true }
);
gBrowser.moveTabTo(newTab, { tabIndex: 1 });
},
{ once: true }
);
gBrowser.pinTab(newTab);
await promise;
for (const tab of tabsToRemove) {
await BrowserTestUtils.removeTab(tab);
}
});

View File

@@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
add_task(async function test_Create_Pinned() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const tabsToRemove = [];
for (let i = 0; i < 3; i++) {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
gBrowser.pinTab(gBrowser.selectedTab);
tabsToRemove.push(gBrowser.selectedTab);
}
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
tabsToRemove.push(gBrowser.selectedTab);
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
const startIndex = pinObject.position;
Assert.greater(startIndex, 0, 'The pin object should have the correct start index');
resolvePromise();
},
{ once: true }
);
gBrowser.pinTab(newTab);
await promise;
for (const tab of tabsToRemove) {
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_Create_Pinned() {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
const tabsToRemove = [];
for (let i = 0; i < 3; i++) {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
gBrowser.pinTab(gBrowser.selectedTab);
tabsToRemove.push(gBrowser.selectedTab);
}
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
tabsToRemove.push(gBrowser.selectedTab);
const newTab = gBrowser.selectedTab;
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
newTab.addEventListener(
'ZenPinnedTabMoved',
async function (event) {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
Assert.equal(
pinObject.position,
0,
'The pin object should have the correct position after moving'
);
resolvePromise();
},
{ once: true }
);
gBrowser.moveTabTo(newTab, { tabIndex: 0 });
},
{ once: true }
);
gBrowser.pinTab(newTab);
await promise;
for (const tab of tabsToRemove) {
await BrowserTestUtils.removeTab(tab);
}
});

View File

@@ -15,26 +15,37 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
gBrowser.pinTab(tab);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(
!tab.hasAttribute('discarded'),
'The tab should not be discarded after being closed'
);
ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -15,26 +15,37 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
gBrowser.pinTab(tab);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(
!tab.hasAttribute('discarded'),
'The tab should not be discarded after being closed'
);
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -12,17 +12,25 @@ add_task(async function test_Pinned_To_Essential() {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
const newTab = gBrowser.selectedTab;
gBrowser.pinTab(newTab);
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
gZenPinnedTabManager.addToEssentials(newTab);
ok(
newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0',
'New tab should be marked as essential.'
gZenPinnedTabManager.addToEssentials(newTab);
ok(
newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0',
'New tab should be marked as essential.'
);
resolvePromise();
},
{ once: true }
);
resolvePromise();
gBrowser.pinTab(newTab);
await promise;
await BrowserTestUtils.removeTab(newTab);

View File

@@ -15,27 +15,35 @@ add_task(async function test_Unload_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
!tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
await promise;
});
});

View File

@@ -15,26 +15,34 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
gBrowser.pinTab(tab);
tab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
const pinTabID = tab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should have a zen-pinned-changed attribute after being pinned'
);
document.getElementById('cmd_close').doCommand();
setTimeout(() => {
ok(
tab.hasAttribute('zen-pinned-changed'),
'The tab should not have a zen-pinned-changed attribute after being closed'
);
ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
resolvePromise();
}, 100);
}, 0);
},
{ once: true }
);
gBrowser.pinTab(tab);
await promise;
});
});

View File

@@ -11,29 +11,36 @@ add_task(async function test_Private_Mode_No_Essentials() {
});
const newTab = gBrowser.selectedTab;
gBrowser.pinTab(newTab);
newTab.addEventListener(
'ZenPinnedTabCreated',
async function (event) {
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
const pinTabID = newTab.getAttribute('zen-pin-id');
ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
try {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
try {
const pins = await ZenPinnedTabsStorage.getPins();
const pinObject = pins.find((pin) => pin.uuid === pinTabID);
ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
await privateWindow.gZenWorkspaces.promiseInitialized;
ok(
!privateWindow.gBrowser.tabs.some((tab) => tab.pinned),
'Private window should not have any pinned tabs initially'
);
let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
await privateWindow.gZenWorkspaces.promiseInitialized;
ok(
!privateWindow.gBrowser.tabs.some((tab) => tab.pinned),
'Private window should not have any pinned tabs initially'
);
await BrowserTestUtils.closeWindow(privateWindow);
} catch (error) {
ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
}
resolvePromise();
await BrowserTestUtils.closeWindow(privateWindow);
} catch (error) {
ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
}
resolvePromise();
},
{ once: true }
);
gZenPinnedTabManager.addToEssentials(newTab);
await promise;

View File

@@ -162,6 +162,7 @@ add_task(async function test_Welcome_Steps() {
);
Assert.equal(tab.group, group, 'Pinned tabs should belong to the first tab group');
}
ok(tab.hasAttribute('zen-pin-id'), 'Pinned tabs should have a zen-pin-id attribute');
}
}
group.delete();

View File

@@ -292,6 +292,9 @@
for (const tab of _tabsToPin) {
tab.setAttribute('zen-workspace-id', gZenWorkspaces.activeWorkspace);
gBrowser.pinTab(tab);
await new Promise((resolve) => {
tab.addEventListener('ZenPinnedTabCreated', resolve, { once: true });
});
}
for (const tab of _tabsToPinEssentials) {
tab.removeAttribute('pending'); // Make it appear loaded

View File

@@ -1318,7 +1318,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
// Use theme from workspace object or passed theme
let workspaceTheme = theme || workspace.theme;
await this.forEachWindow(async (browser) => {
await this.foreachWindowAsActive(async (browser) => {
if (!browser.gZenThemePicker?.promiseInitialized) {
return;
}

View File

@@ -37,14 +37,6 @@ class nsZenWorkspace extends MozXULElement {
`;
}
static get moveTabToButtonMarkup() {
return `
<toolbarbutton class="toolbarbutton-1 chromeclass-toolbar-additional zen-workspaces-actions"
tooltip="dynamic-shortcut-tooltip"
data-l10n-id="zen-move-tab-to-workspace-button" />
`;
}
static get inheritedAttributes() {
return {
'.zen-workspace-tabs-section': 'zen-workspace-id=id',
@@ -96,20 +88,6 @@ class nsZenWorkspace extends MozXULElement {
gZenWorkspaces.changeWorkspaceIcon();
});
if (!gZenWorkspaces.currentWindowIsSyncing) {
let actionsButton = this.indicator.querySelector('.zen-workspaces-actions');
const moveTabToFragment = window.MozXULElement.parseXULToFragment(
nsZenWorkspace.moveTabToButtonMarkup
);
actionsButton.after(moveTabToFragment);
actionsButton.setAttribute('hidden', 'true');
actionsButton = actionsButton.nextElementSibling;
actionsButton.addEventListener('command', (event) => {
event.stopPropagation();
this.#openMoveTabsToWorkspacePanel(event.target);
});
}
this.scrollbox._getScrollableElements = () => {
const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children];
if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) {
@@ -231,7 +209,7 @@ class nsZenWorkspace extends MozXULElement {
if (newName === '') {
return;
}
let workspaces = (await gZenWorkspaces.getWorkspaces()).workspaces;
let workspaces = (await gZenWorkspaces._workspaces()).workspaces;
let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
workspaceData.name = newName;
await gZenWorkspaces.saveWorkspace(workspaceData);
@@ -278,37 +256,6 @@ class nsZenWorkspace extends MozXULElement {
this.style.removeProperty('--toolbox-textcolor');
this.style.removeProperty('--zen-primary-color');
}
#openMoveTabsToWorkspacePanel(button) {
button = button.closest('toolbarbutton');
if (!button) return;
const popup = document.getElementById('zenMoveTabsToSyncedWorkspacePopup');
popup.innerHTML = '';
gZenWorkspaces.getWorkspaces(true).then((workspaces) => {
for (const workspace of workspaces.workspaces) {
const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', async () => {
const { ZenWindowSync } = ChromeUtils.importESModule(
'resource:///modules/zen/ZenWindowSync.sys.mjs'
);
ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid);
});
popup.appendChild(item);
}
button.setAttribute('open', 'true');
popup.addEventListener(
'popuphidden',
() => {
button.removeAttribute('open');
},
{ once: true }
);
popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */);
});
}
}
customElements.define('zen-workspace', nsZenWorkspace);

View File

@@ -126,7 +126,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
}
async #updateIcons() {
const workspaces = await gZenWorkspaces.getWorkspaces();
const workspaces = await gZenWorkspaces._workspaces();
this.innerHTML = '';
for (const workspace of workspaces.workspaces) {
const button = this.#createWorkspaceIcon(workspace);

View File

@@ -9,12 +9,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
/**
* Stores workspace IDs and their last selected tabs.
*/
lastSelectedWorkspaceTabs = {};
#inChangingWorkspace = false;
_lastSelectedWorkspaceTabs = {};
_inChangingWorkspace = false;
draggedElement = null;
#canDebug = Services.prefs.getBoolPref('zen.workspaces.debug', false);
#activeWorkspace = '';
_swipeState = {
isGestureActive: true,
@@ -22,7 +21,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
direction: null,
};
#lastScrollTime = 0;
_lastScrollTime = 0;
bookmarkMenus = [
'PlacesToolbar',
@@ -117,7 +116,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
ChromeUtils.defineLazyGetter(this, 'workspaceIcons', () =>
document.getElementById('zen-workspaces-button')
);
this.#activeWorkspace ||= Services.prefs.getStringPref('zen.workspaces.active', '');
this._activeWorkspace = Services.prefs.getStringPref('zen.workspaces.active', '');
if (this.isPrivateWindow) {
document.documentElement.setAttribute('zen-private-window', 'true');
@@ -129,7 +128,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this.addPopupListeners();
await this.#waitForPromises();
await this.getWorkspaces();
await this._workspaces();
await this.afterLoadInit();
}
@@ -319,14 +318,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _createDefaultWorkspaceIfNeeded() {
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
if (!workspaces.workspaces.length) {
await this.createAndSaveWorkspace('Space', null, true);
this._workspaceCache = null;
}
}
#initializeEmptyTab() {
_initializeEmptyTab() {
for (const tab of gBrowser.tabs) {
// Check if session store has an empty tab
if (tab.hasAttribute('zen-empty-tab') && !tab.pinned) {
@@ -406,51 +405,48 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return document.getElementById(workspaceId);
}
async #initializeTabsStripSections() {
async initializeTabsStripSections() {
await SessionStore.promiseInitialized;
await SessionStore.promiseAllWindowsRestored;
const perifery = document.getElementById('tabbrowser-arrowscrollbox-periphery');
perifery.setAttribute('hidden', 'true');
await new Promise((resolve) => {
setTimeout(() => {
setTimeout(async () => {
await window._zenPromiseNewWindowRestored;
const tabs = gBrowser.tabContainer.allTabs;
const workspaces = await this.getWorkspaces();
for (const workspace of workspaces.workspaces) {
await this._createWorkspaceTabsSection(workspace, tabs);
}
if (tabs.length) {
const defaultSelectedContainer = this.workspaceElement(
this.activeWorkspace
)?.querySelector('.zen-workspace-normal-tabs-section');
const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector(
'.zen-workspace-pinned-tabs-section'
);
// New profile with no workspaces does not have a default selected container
if (defaultSelectedContainer) {
for (const tab of tabs) {
if (tab.hasAttribute('zen-essential')) {
this.getEssentialsSection(tab).appendChild(tab);
continue;
} else if (tab.pinned) {
pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
continue;
}
// before to the last child (perifery)
defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild);
setTimeout(async () => {
const tabs = gBrowser.tabContainer.allTabs;
const workspaces = await this._workspaces();
for (const workspace of workspaces.workspaces) {
await this._createWorkspaceTabsSection(workspace, tabs);
}
if (tabs.length) {
const defaultSelectedContainer = this.workspaceElement(
this.activeWorkspace
)?.querySelector('.zen-workspace-normal-tabs-section');
const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector(
'.zen-workspace-pinned-tabs-section'
);
// New profile with no workspaces does not have a default selected container
if (defaultSelectedContainer) {
for (const tab of tabs) {
if (tab.hasAttribute('zen-essential')) {
this.getEssentialsSection(tab).appendChild(tab);
continue;
} else if (tab.pinned) {
pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
continue;
}
// before to the last child (perifery)
defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild);
}
gBrowser.tabContainer._invalidateCachedTabs();
}
perifery.setAttribute('hidden', 'true');
this._hasInitializedTabsStrip = true;
this.registerPinnedResizeObserver();
this._fixIndicatorsNames(workspaces);
this._resolveSectionsInitialized();
resolve();
});
});
gBrowser.tabContainer._invalidateCachedTabs();
}
perifery.setAttribute('hidden', 'true');
this._hasInitializedTabsStrip = true;
this.registerPinnedResizeObserver();
this._fixIndicatorsNames(workspaces);
this._resolveSectionsInitialized();
resolve();
}, 0);
});
}
@@ -612,7 +608,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
toolbox.addEventListener(
'wheel',
(event) => {
async (event) => {
if (this.privateWindowOrDisabled) return;
// Only process non-gesture scrolls
@@ -638,7 +634,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
const currentTime = Date.now();
if (currentTime - this.#lastScrollTime < scrollCooldown) return;
if (currentTime - this._lastScrollTime < scrollCooldown) return;
//this decides which delta to use
const delta = isVerticalScroll ? event.deltaY : event.deltaX;
@@ -650,7 +646,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
let direction = this.naturalScroll ? -1 : 1;
this.changeWorkspaceShortcut(rawDirection * direction);
this.#lastScrollTime = currentTime;
this._lastScrollTime = currentTime;
},
{ passive: true }
);
@@ -705,7 +701,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
_handleSwipeMayStart(event) {
if (this.privateWindowOrDisabled || this.#inChangingWorkspace) return;
if (this.privateWindowOrDisabled || this._inChangingWorkspace) return;
if (
event.target.closest('#zen-sidebar-foot-buttons') ||
event.target.closest('#urlbar[zen-floating-urlbar="true"]')
@@ -790,11 +786,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
get activeWorkspace() {
return this.#activeWorkspace;
return this._activeWorkspace;
}
set activeWorkspace(value) {
this.#activeWorkspace = value;
this._activeWorkspace = value;
if (this.privateWindowOrDisabled) {
return;
}
@@ -839,16 +835,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return PrivateBrowsingUtils.isWindowPrivate(window);
}
get currentWindowIsSyncing() {
return (
!document.documentElement.hasAttribute('zen-unsynced-window') &&
window._zenStartupSyncFlag !== 'unsynced' &&
!this.isPrivateWindow
);
}
get privateWindowOrDisabled() {
return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing;
return this.isPrivateWindow || !this.shouldHaveWorkspaces;
}
get workspaceEnabled() {
@@ -872,17 +860,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
}
async getWorkspaces(lieToMe = false) {
if (this._workspaceCache && !lieToMe) {
async _workspaces() {
if (this._workspaceCache) {
return this._workspaceCache;
}
if (!this.currentWindowIsSyncing && !lieToMe) {
if (this.isPrivateWindow) {
this._workspaceCache = {
workspaces: this._tempWorkspace ? [this._tempWorkspace] : [],
workspaces: this._privateWorkspace ? [this._privateWorkspace] : [],
lastChangeTimestamp: 0,
};
this.#activeWorkspace = this._tempWorkspace?.uuid;
this._activeWorkspace = this._privateWorkspace?.uuid;
return this._workspaceCache;
}
@@ -895,23 +883,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Get the active workspace ID from preferences
const activeWorkspaceId = this.activeWorkspace;
if (!lieToMe) {
if (activeWorkspaceId) {
const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId);
// Set the active workspace ID to the first one if the one with selected id doesn't exist
if (!activeWorkspace) {
this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
}
} else {
// Set the active workspace ID to the first one if active workspace doesn't exist
if (activeWorkspaceId) {
const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId);
// Set the active workspace ID to the first one if the one with selected id doesn't exist
if (!activeWorkspace) {
this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
}
} else {
// Set the active workspace ID to the first one if active workspace doesn't exist
this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
}
// sort by position
this._workspaceCache.workspaces.sort(
(a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)
);
return this._workspaceCache;
}
@@ -951,14 +937,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
console.error('gZenWorkspaces: Error initializing theme picker', e);
}
await this.workspaceBookmarks();
await this.#initializeTabsStripSections();
this.#initializeEmptyTab();
await this.initializeTabsStripSections();
this._initializeEmptyTab();
await gZenPinnedTabManager.refreshPinnedTabs({ init: true });
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
this._resolveInitialized();
this.#clearAnyZombieTabs(); // Dont call with await
delete window._zenPromiseNewWindowRestored;
const tabUpdateListener = this.updateTabsContainers.bind(this);
window.addEventListener('TabOpen', tabUpdateListener);
@@ -1126,15 +1112,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
shouldCloseWindow() {
return (
!window.toolbar.visible ||
Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') ||
this.privateWindowOrDisabled
!window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab')
);
}
async #clearAnyZombieTabs() {
const tabs = this.allStoredTabs;
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
for (let tab of tabs) {
const workspaceID = tab.getAttribute('zen-workspace-id');
if (
@@ -1222,24 +1206,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
);
}
generateMenuItemForWorkspace(workspace) {
const item = document.createXULElement('menuitem');
item.className = 'zen-workspace-context-menu-item';
item.setAttribute('zen-workspace-id', workspace.uuid);
item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
let name = workspace.name;
const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
name = `${workspace.icon} ${name}`;
}
item.setAttribute('label', name);
if (iconIsSvg) {
item.setAttribute('image', workspace.icon);
item.classList.add('zen-workspace-context-icon');
}
return item;
}
#contextMenuData = null;
updateWorkspaceActionsMenu(event) {
if (event.target.id !== 'zenWorkspaceMoreActions') {
@@ -1283,7 +1249,20 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (!this.#contextMenuData.workspaceId) {
separator.hidden = false;
for (const workspace of [...this._workspaceCache.workspaces].reverse()) {
const item = this.generateMenuItemForWorkspace(workspace);
const item = document.createXULElement('menuitem');
item.className = 'zen-workspace-context-menu-item';
item.setAttribute('zen-workspace-id', workspace.uuid);
item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
let name = workspace.name;
const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
name = `${workspace.icon} ${name}`;
}
item.setAttribute('label', name);
if (iconIsSvg) {
item.setAttribute('image', workspace.icon);
item.classList.add('zen-workspace-context-icon');
}
item.addEventListener('command', (e) => {
this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id'));
});
@@ -1328,12 +1307,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async removeWorkspace(windowID) {
let workspacesData = await this.getWorkspaces();
let workspacesData = await this._workspaces();
await this.changeWorkspace(
workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID)
);
await this.#deleteAllTabsInWorkspace(windowID);
delete this.lastSelectedWorkspaceTabs[windowID];
delete this._lastSelectedWorkspaceTabs[windowID];
await ZenWorkspacesStorage.removeWorkspace(windowID);
// Remove the workspace from the cache
this._workspaceCache.workspaces = this._workspaceCache.workspaces.filter(
@@ -1351,7 +1330,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async getActiveWorkspace() {
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
return (
workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ??
workspaces.workspaces[0]
@@ -1385,7 +1364,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
browser.gZenWorkspaces._workspaceCache = null;
browser.gZenWorkspaces._workspaceBookmarksCache = null;
}
let workspaces = await browser.gZenWorkspaces.getWorkspaces();
let workspaces = await browser.gZenWorkspaces._workspaces();
browser.document
.getElementById('cmd_zenCtxDeleteWorkspace')
.setAttribute('disabled', workspaces.workspaces.length <= 1);
@@ -1410,7 +1389,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) {
const currentWindowIsPrivate = !this.currentWindowIsSyncing;
const currentWindowIsPrivate = this.isPrivateWindow;
if (onInit) {
if (currentWindowIsPrivate) return;
return await this._propagateWorkspaceDataForWindow(this.ownerWindow, {
@@ -1423,7 +1402,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// For example, when the window is in private browsing mode.
if (
!browser.gZenWorkspaces.workspaceEnabled ||
!browser.gZenWorkspaces.currentWindowIsSyncing !== currentWindowIsPrivate
browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate
) {
return;
}
@@ -1438,7 +1417,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (this.privateWindowOrDisabled) {
return;
}
const workspaces = (await this.getWorkspaces()).workspaces;
const workspaces = (await this._workspaces()).workspaces;
const workspace = workspaces.find((w) => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
@@ -1464,7 +1443,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) {
const workspaces = (await this.getWorkspaces()).workspaces;
const workspaces = (await this._workspaces()).workspaces;
const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId);
const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0];
const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId);
@@ -1499,6 +1478,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!tab.hasAttribute('zen-empty-tab') &&
!tab.hasAttribute('zen-essential')
);
for (const tab of tabs) {
if (tab.pinned) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
}
}
gBrowser.removeTabs(tabs, {
animate: false,
skipSessionStore: true,
@@ -1646,19 +1630,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspace(workspace, ...args) {
if (
!this.workspaceEnabled ||
this.#inChangingWorkspace ||
this._inChangingWorkspace ||
gNavToolbox.hasAttribute('movingtab')
) {
return;
}
this.#inChangingWorkspace = true;
this._inChangingWorkspace = true;
try {
this.log('Changing workspace to', workspace?.uuid);
await this._performWorkspaceChange(workspace, ...args);
} catch (e) {
console.error('gZenWorkspaces: Error changing workspace', e);
}
this.#inChangingWorkspace = false;
this._inChangingWorkspace = false;
}
_cancelSwipeAnimation() {
@@ -1677,7 +1661,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
gZenFolders.cancelPopupTimer();
// Refresh tab cache
@@ -1763,7 +1747,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
) {
if (
workspaceElement &&
!(this.#inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop)
!(this._inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop)
) {
delete this._alwaysAnimatePaddingTop;
const essentialsHeight = essentialContainer.getBoundingClientRect().height;
@@ -1793,7 +1777,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
this._organizingWorkspaceStrip = true;
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid);
if (!justMove) {
this._fixIndicatorsNames(workspaces);
@@ -1876,7 +1860,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const grainValue =
minGrain +
(maxGrain - minGrain) * (existingGrain > nextGrain ? 1 - percentage : percentage);
if (!this.#inChangingWorkspace) {
if (!this._inChangingWorkspace) {
gZenThemePicker.updateNoise(grainValue);
}
}
@@ -1927,7 +1911,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const kGlobalAnimationDuration = 0.2;
this._animatingChange = true;
const animations = [];
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex;
const clonedEssentials = [];
@@ -2230,7 +2214,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
tab,
currentWorkspace.uuid,
currentWorkspace.containerTabId,
await this.getWorkspaces()
await this._workspaces()
);
}
@@ -2286,14 +2270,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async _handleTabSelection(workspace, onInit, previousWorkspaceId) {
const currentSelectedTab = gBrowser.selectedTab;
const oldWorkspaceId = previousWorkspaceId;
const lastSelectedTab = this.lastSelectedWorkspaceTabs[workspace.uuid];
const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid];
const containerId = workspace.containerTabId?.toString();
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
// Save current tab as last selected for old workspace if it shouldn't be visible in new workspace
if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) {
this.lastSelectedWorkspaceTabs[oldWorkspaceId] =
this._lastSelectedWorkspaceTabs[oldWorkspaceId] =
gZenGlanceManager.getTabOrGlanceParent(currentSelectedTab);
}
@@ -2422,7 +2406,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async _updateWorkspacesChangeContextMenu() {
if (gZenWorkspaces.privateWindowOrDisabled) return;
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup');
if (!menuPopup) {
@@ -2472,8 +2456,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (!this.workspaceEnabled) {
return;
}
if (!this.currentWindowIsSyncing) {
name = this.isPrivateWindow ? 'Private ' + name : 'Temporary';
if (this.isPrivateWindow) {
name = 'Private ' + name;
}
// get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace
const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter(
@@ -2490,8 +2474,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!dontChange,
containerTabId
);
if (!this.currentWindowIsSyncing) {
this._tempWorkspace = workspaceData;
if (this.isPrivateWindow) {
this._privateWorkspace = workspaceData;
} else {
await this.saveWorkspace(workspaceData, dontChange);
}
@@ -2653,7 +2637,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (workspaceID) {
if (tab.hasAttribute('change-workspace') && this.moveTabToWorkspace(tab, workspaceID)) {
this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
tab.removeAttribute('change-workspace');
const workspace = this.getWorkspaceFromId(workspaceID);
setTimeout(() => {
@@ -2685,7 +2669,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async onLocationChange(event) {
let tab = event.target;
this.#changeToEmptyTab();
if (!this.workspaceEnabled || this.#inChangingWorkspace || this._isClosingWindow) {
if (!this.workspaceEnabled || this._inChangingWorkspace || this._isClosingWindow) {
return;
}
@@ -2709,7 +2693,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Only update last selected tab for non-essential tabs in their workspace
if (workspaceID === activeWorkspace.uuid) {
this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
}
// Switch workspace if needed
@@ -2726,7 +2710,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Context menu management
async contextChangeContainerTab(event) {
this._organizingWorkspaceStrip = true;
let workspaces = await this.getWorkspaces();
let workspaces = await this._workspaces();
let workspace = workspaces.workspaces.find(
(workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace)
);
@@ -2780,7 +2764,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspaceShortcut(offset = 1, whileScrolling = false) {
// Cycle through workspaces
let workspaces = await this.getWorkspaces();
let workspaces = await this._workspaces();
let activeWorkspace = await this.getActiveWorkspace();
let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace);
@@ -2834,17 +2818,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const previousWorkspaceID = document.documentElement.getAttribute('zen-workspace-id');
for (let tab of tabs) {
this.moveTabToWorkspace(tab, workspaceID);
if (this.lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) {
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];
delete this._lastSelectedWorkspaceTabs[previousWorkspaceID];
}
}
// Make sure we select the last tab in the new workspace
this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
tabs[tabs.length - 1]
);
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
await this.changeWorkspace(
workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)
);
@@ -2903,7 +2887,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async shortcutSwitchTo(index) {
const workspaces = await this.getWorkspaces();
const workspaces = await this._workspaces();
// The index may be out of bounds, if it doesnt exist, don't do anything
if (index >= workspaces.workspaces.length || index < 0) {
return;
@@ -3110,7 +3094,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this._workspaceChangeInProgress = true;
try {
this.lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] =
this._lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] =
gZenGlanceManager.getTabOrGlanceParent(tab);
await this.changeWorkspace(workspaceToSwitch);
} finally {

View File

@@ -33,7 +33,7 @@ zen-workspace-creation {
width: calc(100% - 10px);
margin: auto;
gap: 3.2rem;
margin-top: 0.6rem;
margin-top: 1.2rem;
height: 100%;
& .zen-workspace-creation-form {

View File

@@ -181,11 +181,7 @@
height: calc(100% - var(--zen-toolbox-padding) * 2);
}
:root[zen-private-window] & {
pointer-events: none;
}
:root:not([zen-unsynced-window]) & {
:root:not([zen-private-window]) & {
&:hover,
&[open='true'] {
&::before {
@@ -232,19 +228,15 @@
font-weight: 600;
align-items: center;
margin: 0;
:root[zen-unsynced-window] & {
pointer-events: none;
}
}
.zen-workspaces-actions {
margin-left: auto !important;
opacity: 0;
visibility: collapse;
transition: opacity 0.1s;
order: 5;
--toolbarbutton-inner-padding: 6px !important;
opacity: 0;
visibility: collapse;
& image {
border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important;
@@ -256,24 +248,9 @@
:root[zen-renaming-tab='true'] & {
display: none;
}
:root[zen-unsynced-window] & {
.toolbarbutton-text {
display: flex;
font-size: 10px;
min-height: 22px;
}
.toolbarbutton-icon {
display: none;
}
}
}
:root[zen-unsynced-window='true']
#navigator-toolbox[zen-has-implicit-hover='true']
& .zen-workspaces-actions,
:root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions,
:root:not([zen-private-window]) &:hover .zen-workspaces-actions,
& .zen-workspaces-actions[open='true'] {
visibility: visible;
pointer-events: auto;

View File

@@ -26,6 +26,7 @@ export default [
'ZEN_KEYSET_ID',
'gZenPinnedTabManager',
'ZenPinnedTabsStorage',
'gZenEmojiPicker',
'gZenSessionStore',

View File

@@ -19,7 +19,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.17.13b",
"displayVersion": "1.17.14b",
"github": {
"repo": "zen-browser/desktop"
},