Compare commits

..
4 Commits
19 changed files with 613 additions and 209 deletions
@@ -78,6 +78,7 @@ library-downloads-opening-in = { PLATFORM() ->
[windows] Opening in File Explorer…
*[other] Opening in file manager…
}
library-downloads-open-when-done = Opens when finished
library-downloads-cancel-button =
.title = Cancel download
library-downloads-filter-title = Filter Downloads…
+2 -10
View File
@@ -1,8 +1,8 @@
diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
index 0aa4baea8e5e6d742fc6cd3333068e7eb64b5bbc..057bb2386420edd31154979338e71d00a51a7266 100644
index 2f3f46d8d82dc90ebca8d64ace9d6bf1ee127937..163eb027b8cca1d43af24fff762e951e6f1030fc 100644
--- a/browser/base/content/browser-init.js
+++ b/browser/base/content/browser-init.js
@@ -307,6 +307,7 @@ var gBrowserInit = {
@@ -279,6 +279,7 @@ var gBrowserInit = {
},
onLoad() {
@@ -10,11 +10,3 @@ index 0aa4baea8e5e6d742fc6cd3333068e7eb64b5bbc..057bb2386420edd31154979338e71d00
gBrowser.addEventListener("DOMUpdateBlockedPopups", e =>
PopupAndRedirectBlockerObserver.handleEvent(e)
);
@@ -501,6 +502,7 @@ var gBrowserInit = {
this._handleURIToLoad();
+ gZenWorkspaces.selectStartPage();
Services.obs.addObserver(gIdentityHandler, "perm-changed");
Services.obs.addObserver(gRemoteControl, "devtools-socket");
Services.obs.addObserver(gRemoteControl, "marionette-listening");
@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eadac7510935 100644
index 52fc803050cd257d6de52b1e4a92f3211cbaba88..ecfcef5dabb7056886972651cd35835564684a0e 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -111,6 +111,11 @@ const TAB_EVENTS = [
@@ -308,20 +308,37 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
tabbrowser.removeTab(tabbrowser.tabs[i]);
}
}
@@ -5529,6 +5580,12 @@ class _SessionStore {
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
);
}
@@ -5504,6 +5555,11 @@ class _SessionStore {
this.#prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") &&
this.#restore_on_demand;
+ if (winData.isZenUnsynced) {
+ aWindow.document.documentElement.setAttribute("zen-unsynced-window", "true");
+ }
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData);
+
this.#log.debug(
`restoreWindow, will restore ${winData.tabs.length} tabs and ${
winData.groups?.length ?? 0
@@ -5529,6 +5585,8 @@ class _SessionStore {
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
);
}
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
// Move the originally open tabs to the end.
if (initialTabs) {
@@ -6098,6 +6155,25 @@ class _SessionStore {
@@ -5630,6 +5688,8 @@ class _SessionStore {
// set smoothScroll back to the original value
arrowScrollbox.smoothScroll = smoothScroll;
+ aWindow.gZenWorkspaces?.onWindowRestored();
+
Glean.sessionRestore.restoreWindow.stopAndAccumulate(timerId);
}
@@ -6098,6 +6158,25 @@ class _SessionStore {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -347,7 +364,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -6272,6 +6348,9 @@ class _SessionStore {
@@ -6272,6 +6351,9 @@ class _SessionStore {
aWindow.gURLBar.readOnly = false;
}
}
@@ -357,7 +374,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
let promiseParts = Promise.withResolvers();
const wasMinimized = aWindow.windowState == aWindow.STATE_MINIMIZED;
@@ -6774,6 +6853,7 @@ class _SessionStore {
@@ -6774,6 +6856,7 @@ class _SessionStore {
*/
#isCmdLineEmpty(aWindow, aState) {
var pinnedOnly =
@@ -365,7 +382,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
aState.windows &&
aState.windows.every(win => win.tabs.every(tab => tab.pinned));
@@ -7116,7 +7196,7 @@ class _SessionStore {
@@ -7116,7 +7199,7 @@ class _SessionStore {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -374,7 +391,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7131,7 +7211,7 @@ class _SessionStore {
@@ -7131,7 +7214,7 @@ class _SessionStore {
);
// We don't want to increment tIndex here.
continue;
@@ -383,7 +400,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
// Convert any open groups into saved groups.
let groupStateToSave = window.groups.find(
groupState => groupState.id == window.tabs[tIndex].groupId
@@ -7691,7 +7771,6 @@ class _SessionStore {
@@ -7691,7 +7774,6 @@ class _SessionStore {
timer.initWithCallback(
() => {
if (beats <= 0) {
@@ -391,7 +408,7 @@ index 52fc803050cd257d6de52b1e4a92f3211cbaba88..a9841832ddce540dc0f9b14c29e5eada
Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
deferred.resolve();
}
@@ -8174,6 +8253,7 @@ class _SessionStore {
@@ -8174,6 +8256,7 @@ class _SessionStore {
if (
!savedTabGroupState.tabs.length ||
this.getSavedTabGroup(savedTabGroupState.id)
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/Tabbrowser.sys.mjs b/browser/components/tabbrowser/Tabbrowser.sys.mjs
index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089db94a6d3 100644
index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..ddb15aeafab54a595b8f710b2234f21411d4e99a 100644
--- a/browser/components/tabbrowser/Tabbrowser.sys.mjs
+++ b/browser/components/tabbrowser/Tabbrowser.sys.mjs
@@ -484,6 +484,7 @@ export class Tabbrowser {
@@ -504,7 +504,24 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
});
}
@@ -4943,8 +5121,9 @@ export class Tabbrowser {
@@ -4889,6 +5067,16 @@ export class Tabbrowser {
) {
let tabs = [];
let tabsFragment = this.document.createDocumentFragment();
+ let zenWorkspaces = this.documentGlobal.gZenWorkspaces;
+ let zenCollectsFragments =
+ !!zenWorkspaces?.canCollectSessionRestoreFragments;
+ if (zenCollectsFragments) {
+ zenWorkspaces.beginSessionRestoreFragments();
+ }
+ let zenFragmentFor = tabData =>
+ zenCollectsFragments
+ ? zenWorkspaces.getSessionRestoreFragment(tabData)
+ : tabsFragment;
let tabToSelect = null;
let hiddenTabs = new Map();
/** @type {Map<TabGroupId, TabGroupWorkingData>} */
@@ -4943,8 +5131,9 @@ export class Tabbrowser {
}
// Add a new tab if needed.
@@ -516,7 +533,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let url = "about:blank";
if (tabData.entries?.length) {
@@ -4977,8 +5156,10 @@ export class Tabbrowser {
@@ -4977,8 +5166,10 @@ export class Tabbrowser {
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -528,7 +545,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (select) {
tabToSelect = tab;
}
@@ -5000,7 +5181,8 @@ export class Tabbrowser {
@@ -5000,7 +5191,8 @@ export class Tabbrowser {
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this.#fireTabOpen(tab, {});
@@ -538,7 +555,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -5020,7 +5202,10 @@ export class Tabbrowser {
@@ -5020,9 +5212,19 @@ export class Tabbrowser {
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -548,9 +565,55 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
+ tabGroup.stateData.essential,
+ tabGroup.stateData.splitView,
);
tabsFragment.appendChild(tabGroup.node);
- tabsFragment.appendChild(tabGroup.node);
+ // A group lives in the space of the tabs it holds, but its own
+ // pinned and essential state is what decides its section.
+ zenFragmentFor({
+ zenWorkspace: tabData.zenWorkspace,
+ userContextId: tabData.userContextId,
+ pinned: tabGroup.stateData.pinned ?? tabData.pinned,
+ zenEssential: tabGroup.stateData.essential ?? tabData.zenEssential,
+ }).appendChild(tabGroup.node);
}
@@ -5080,9 +5265,18 @@ export class Tabbrowser {
}
} else {
@@ -5032,14 +5234,14 @@ export class Tabbrowser {
}
if (!splitView) {
- tabsFragment.appendChild(tab);
+ zenFragmentFor(tabData).appendChild(tab);
} else if (splitView?.node) {
// Treat split views as a unit for hiding purposes -- both tabs and
// the split view wrapper itself should be hidden.
if (splitView.tabs.some(t => t.hidden)) {
splitView.node.toggleAttribute("hidden", true);
}
- tabsFragment.appendChild(splitView.node);
+ zenFragmentFor(tabData).appendChild(splitView.node);
}
if (tabWasReused) {
@@ -5051,7 +5253,17 @@ export class Tabbrowser {
}
// inject the top-level tab and tab group DOM nodes
- this.tabContainer.appendChild(tabsFragment);
+ if (zenCollectsFragments) {
+ zenWorkspaces.flushSessionRestoreFragments();
+ // allTabs only reaches the active space, so #updateTabsAfterInsert below
+ // would leave every tab restored into another space without a position.
+ // Session order is what SessionStore indexes these tabs by anyway.
+ for (let index = 0; index < tabs.length; index++) {
+ tabs[index]._index = index;
+ }
+ } else {
+ this.tabContainer.appendChild(tabsFragment);
+ }
// inject tab DOM nodes into the now-connected tab group and split view DOM nodes
for (const tabGroup of tabGroupWorkingData.values()) {
@@ -5080,9 +5292,18 @@ export class Tabbrowser {
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -569,7 +632,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (tabs.length > 1 || !tabs[0].selected) {
this.#updateTabsAfterInsert();
@@ -5313,11 +5507,17 @@ export class Tabbrowser {
@@ -5313,11 +5534,17 @@ export class Tabbrowser {
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -588,7 +651,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let insertRelatedAfterCurrent = Services.prefs.getBoolPref(
"browser.tabs.insertRelatedAfterCurrent"
);
@@ -5332,7 +5532,7 @@ export class Tabbrowser {
@@ -5332,7 +5559,7 @@ export class Tabbrowser {
(insertRelatedAfterCurrent && lastRelatedTab) ||
openerTab ||
this.selectedTab;
@@ -597,7 +660,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
tabGroup = previousTab.group;
}
if (
@@ -5348,7 +5548,7 @@ export class Tabbrowser {
@@ -5348,7 +5575,7 @@ export class Tabbrowser {
previousTab.splitview
) + 1;
} else if (previousTab.visible) {
@@ -606,7 +669,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
} else if (previousTab == this.documentGlobal.FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -5376,14 +5576,14 @@ export class Tabbrowser {
@@ -5376,14 +5603,14 @@ export class Tabbrowser {
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -625,7 +688,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -5400,7 +5600,7 @@ export class Tabbrowser {
@@ -5400,7 +5627,7 @@ export class Tabbrowser {
this.tabContainer._invalidateCachedTabs();
@@ -634,7 +697,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
this.isSplitViewWrapper(itemAfter)
@@ -5431,7 +5631,11 @@ export class Tabbrowser {
@@ -5431,7 +5658,11 @@ export class Tabbrowser {
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -646,7 +709,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
if (tab.group?.collapsed) {
@@ -5446,6 +5650,7 @@ export class Tabbrowser {
@@ -5446,6 +5677,7 @@ export class Tabbrowser {
if (pinned) {
this.#updateTabBarForPinnedTabs();
}
@@ -654,7 +717,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
this.documentGlobal.TabBarVisibility.update();
}
@@ -6025,6 +6230,7 @@ export class Tabbrowser {
@@ -6025,6 +6257,7 @@ export class Tabbrowser {
metricsContext,
} = {}
) {
@@ -662,7 +725,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -6135,6 +6341,7 @@ export class Tabbrowser {
@@ -6135,6 +6368,7 @@ export class Tabbrowser {
closedTabCount -= 1;
}
}
@@ -670,7 +733,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (closedTabCount > 0) {
this.recordTabMetrics(
@@ -6236,6 +6443,14 @@ export class Tabbrowser {
@@ -6236,6 +6470,14 @@ export class Tabbrowser {
return;
}
@@ -685,7 +748,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let isVisibleTab = aTab.visible;
// We have to sample the tab width now, since #beginRemoveTab might
// end up modifying the DOM in such a way that aTab gets a new
@@ -6244,6 +6459,9 @@ export class Tabbrowser {
@@ -6244,6 +6486,9 @@ export class Tabbrowser {
let tabWidth =
this.documentGlobal.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -695,7 +758,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (
!this.#beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -6254,13 +6472,14 @@ export class Tabbrowser {
@@ -6254,13 +6499,14 @@ export class Tabbrowser {
metricsContext,
})
) {
@@ -711,7 +774,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let lockTabSizing =
!this.tabContainer.verticalMode &&
!aTab.pinned &&
@@ -6291,7 +6510,13 @@ export class Tabbrowser {
@@ -6291,7 +6537,13 @@ export class Tabbrowser {
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -726,7 +789,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
return;
}
@@ -6331,7 +6556,9 @@ export class Tabbrowser {
@@ -6331,7 +6583,9 @@ export class Tabbrowser {
get #shouldCloseWindowWithLastTab() {
return (
!this.documentGlobal.toolbar.visible ||
@@ -737,7 +800,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
);
}
@@ -6347,7 +6574,7 @@ export class Tabbrowser {
@@ -6347,7 +6601,7 @@ export class Tabbrowser {
*/
#isLastTabInWindow(tab) {
for (const otherTab of this.tabs) {
@@ -746,7 +809,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
return false;
}
}
@@ -6488,6 +6715,7 @@ export class Tabbrowser {
@@ -6488,6 +6742,7 @@ export class Tabbrowser {
newTab = true;
}
@@ -754,7 +817,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -6542,13 +6770,7 @@ export class Tabbrowser {
@@ -6542,13 +6797,7 @@ export class Tabbrowser {
}
if (newTab) {
@@ -769,7 +832,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
} else {
this.documentGlobal.TabBarVisibility.update();
}
@@ -6701,6 +6923,7 @@ export class Tabbrowser {
@@ -6701,6 +6950,7 @@ export class Tabbrowser {
this.tabs[i]._index = i;
}
@@ -777,7 +840,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (!this.#windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -6891,6 +7114,7 @@ export class Tabbrowser {
@@ -6891,6 +7141,7 @@ export class Tabbrowser {
memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed,
});
@@ -785,7 +848,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
/**
@@ -6937,11 +7161,12 @@ export class Tabbrowser {
@@ -6937,11 +7188,12 @@ export class Tabbrowser {
}
let excludeTabs = new Set(aExcludeTabs);
@@ -799,7 +862,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
if (
@@ -6949,13 +7174,13 @@ export class Tabbrowser {
@@ -6949,13 +7201,13 @@ export class Tabbrowser {
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -815,7 +878,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
);
if (Services.prefs.getBoolPref("browser.tabs.selectMRUOnClose", false)) {
@@ -6970,6 +7195,13 @@ export class Tabbrowser {
@@ -6970,6 +7222,13 @@ export class Tabbrowser {
}
}
@@ -829,7 +892,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let tab = this.tabContainer.findNextTab(aTab, {
direction: 1,
filter: _tab => remainingTabs.includes(_tab),
@@ -6983,7 +7215,7 @@ export class Tabbrowser {
@@ -6983,7 +7242,7 @@ export class Tabbrowser {
}
if (tab) {
@@ -838,7 +901,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -7004,7 +7236,7 @@ export class Tabbrowser {
@@ -7004,7 +7263,7 @@ export class Tabbrowser {
});
}
@@ -847,7 +910,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
#blurTab(aTab) {
@@ -7021,7 +7253,7 @@ export class Tabbrowser {
@@ -7021,7 +7280,7 @@ export class Tabbrowser {
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -856,7 +919,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -7080,6 +7312,7 @@ export class Tabbrowser {
@@ -7080,6 +7339,7 @@ export class Tabbrowser {
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -864,7 +927,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
!remoteBrowser.#beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -7091,7 +7324,7 @@ export class Tabbrowser {
@@ -7091,7 +7351,7 @@ export class Tabbrowser {
// 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.
@@ -873,7 +936,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (closeWindow) {
let win = aOtherTab.documentGlobal;
win.windowUtils.suppressAnimation(true);
@@ -7231,11 +7464,13 @@ export class Tabbrowser {
@@ -7231,11 +7491,13 @@ export class Tabbrowser {
}
// Finish tearing down the tab that's going away.
@@ -887,7 +950,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
this.setTabTitle(aOurTab);
@@ -7484,10 +7719,10 @@ export class Tabbrowser {
@@ -7484,10 +7746,10 @@ export class Tabbrowser {
}
}
@@ -900,7 +963,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -7576,7 +7811,8 @@ export class Tabbrowser {
@@ -7576,7 +7838,8 @@ export class Tabbrowser {
* @param {object} [aOptions={}]
* Key-value pairs that will be serialized into the features string.
*/
@@ -910,7 +973,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (this.tabs.length == 1) {
return null;
}
@@ -7593,7 +7829,7 @@ export class Tabbrowser {
@@ -7593,7 +7856,7 @@ export class Tabbrowser {
// tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab);
@@ -919,7 +982,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
private: lazy.PrivateBrowsingUtils.isWindowPrivate(this.documentGlobal),
features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`)
@@ -7601,6 +7837,8 @@ export class Tabbrowser {
@@ -7601,6 +7864,8 @@ export class Tabbrowser {
openerWindow: this.documentGlobal,
args,
});
@@ -928,7 +991,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
/**
@@ -7734,7 +7972,7 @@ export class Tabbrowser {
@@ -7734,7 +7999,7 @@ export class Tabbrowser {
* @returns {element is MozTabbrowserTabGroup}
*/
isTabGroup(element) {
@@ -937,7 +1000,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
/**
@@ -7807,8 +8045,8 @@ export class Tabbrowser {
@@ -7807,8 +8072,8 @@ export class Tabbrowser {
}
// Don't allow mixing pinned and unpinned tabs.
@@ -948,7 +1011,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -7854,8 +8092,8 @@ export class Tabbrowser {
@@ -7854,8 +8119,8 @@ export class Tabbrowser {
this.#handleTabMove(
element,
() => {
@@ -959,7 +1022,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
neighbor = neighbor.group;
}
if (neighbor?.splitview) {
@@ -7866,6 +8104,12 @@ export class Tabbrowser {
@@ -7866,6 +8131,12 @@ export class Tabbrowser {
return;
}
}
@@ -972,7 +1035,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
if (movingForwards && neighbor) {
neighbor.after(element);
@@ -7939,23 +8183,31 @@ export class Tabbrowser {
@@ -7939,23 +8210,31 @@ export class Tabbrowser {
) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -1010,7 +1073,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
} 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
@@ -7968,12 +8220,35 @@ export class Tabbrowser {
@@ -7968,12 +8247,35 @@ export class Tabbrowser {
// 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.
@@ -1047,7 +1110,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
// We want to include the splitview wrapper if it's the targetElement, but
// not in the case where we want to reverse tabs within the same splitview.
@@ -7982,6 +8257,7 @@ export class Tabbrowser {
@@ -7982,6 +8284,7 @@ export class Tabbrowser {
}
let getContainer = () =>
@@ -1055,7 +1118,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -7990,11 +8266,15 @@ export class Tabbrowser {
@@ -7990,11 +8293,15 @@ export class Tabbrowser {
element,
() => {
if (moveBefore) {
@@ -1072,7 +1135,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
},
{ metricsContext }
@@ -8070,11 +8350,15 @@ export class Tabbrowser {
@@ -8070,11 +8377,15 @@ export class Tabbrowser {
* The context for the operation for telemetry purposes.
*/
moveTabToExistingGroup(aTab, aGroup, { metricsContext } = {}) {
@@ -1091,7 +1154,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
}
if (aTab.group && aTab.group.id === aGroup.id) {
return;
@@ -8153,6 +8437,7 @@ export class Tabbrowser {
@@ -8153,6 +8464,7 @@ export class Tabbrowser {
let state = {
tabIndex: tab.index,
@@ -1099,7 +1162,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -8190,7 +8475,7 @@ export class Tabbrowser {
@@ -8190,7 +8502,7 @@ export class Tabbrowser {
let changedSplitView =
previousTabState.splitViewId != currentTabState.splitViewId;
@@ -1108,7 +1171,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
tab.dispatchEvent(
new this.documentGlobal.CustomEvent("TabMove", {
bubbles: true,
@@ -8248,6 +8533,10 @@ export class Tabbrowser {
@@ -8248,6 +8560,10 @@ export class Tabbrowser {
moveActionCallback();
@@ -1119,7 +1182,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -8307,7 +8596,22 @@ export class Tabbrowser {
@@ -8307,7 +8623,22 @@ export class Tabbrowser {
* @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted.
*/
@@ -1143,7 +1206,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
// Swap the dropped tab with a new one we create and then close
// it in the other window (making it seem to have moved between
// windows). We also ensure that the tab we create to swap into has
@@ -8350,6 +8654,8 @@ export class Tabbrowser {
@@ -8350,6 +8681,8 @@ export class Tabbrowser {
}
params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params);
@@ -1152,7 +1215,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -9189,7 +9495,7 @@ export class Tabbrowser {
@@ -9189,7 +9522,7 @@ export class Tabbrowser {
// preventDefault(). It will still raise the window if appropriate.
return;
}
@@ -1161,7 +1224,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
this.documentGlobal.focus();
aEvent.preventDefault();
}
@@ -9206,7 +9512,6 @@ export class Tabbrowser {
@@ -9206,7 +9539,6 @@ export class Tabbrowser {
on_TabGroupCollapse(aEvent) {
aEvent.target.tabs.forEach(tab => {
@@ -1169,7 +1232,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
});
}
@@ -9554,7 +9859,9 @@ export class Tabbrowser {
@@ -9554,7 +9886,9 @@ export class Tabbrowser {
let filter = this.#tabFilters.get(tab);
if (filter) {
@@ -1179,7 +1242,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
let listener = this.#tabListeners.get(tab);
if (listener) {
@@ -10354,6 +10661,7 @@ class TabProgressListener {
@@ -10354,6 +10688,7 @@ class TabProgressListener {
aWebProgress.isTopLevel
) {
this._tab.setAttribute("busy", "true");
@@ -1187,7 +1250,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
this.#tabbrowser._tabAttrModified(this._tab, ["busy"]);
this._tab._notselectedsinceload = !this._tab.selected;
}
@@ -10434,6 +10742,7 @@ class TabProgressListener {
@@ -10434,6 +10769,7 @@ class TabProgressListener {
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
@@ -1195,7 +1258,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..a9a5dfbc38348f2c070f22af2954a089
!this._browser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
@@ -10610,13 +10919,6 @@ class TabProgressListener {
@@ -10610,13 +10946,6 @@ class TabProgressListener {
this._browser.originalURI = aRequest.originalURI;
}
+12 -14
View File
@@ -73,6 +73,10 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
this.#changePage(false);
} else if (event.target.id === "PanelUI-zen-emojis-picker-change-svg") {
this.#changePage(true);
} else if (event.target.classList.contains("zen-emojis-picker-emoji")) {
this.#selectEmoji(event.target.getAttribute("label"));
} else if (event.target.classList.contains("zen-emojis-picker-svg")) {
this.#selectEmoji(this.getSVGURL(event.target.getAttribute("icon")));
}
break;
case "input":
@@ -176,19 +180,17 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
this.searchInput.value = "";
const allowEmojis = !this.#panel.hasAttribute("only-svg-icons");
if (allowEmojis) {
const emojiList = this.emojiList;
const emojis = document.createDocumentFragment();
for (const emoji of this.#emojis) {
const item = document.createXULElement("toolbarbutton");
item.className = "toolbarbutton-1 zen-emojis-picker-emoji";
item.setAttribute("label", emoji.emoji);
item.setAttribute("tooltiptext", "");
item.addEventListener("command", () => {
this.#selectEmoji(emoji.emoji);
});
emojiList.appendChild(item);
emojis.appendChild(item);
}
this.emojiList.replaceChildren(emojis);
}
const svgList = this.svgList;
const icons = document.createDocumentFragment();
for (const icon of SVG_ICONS) {
const item = document.createXULElement("toolbarbutton");
item.className = "toolbarbutton-1 zen-emojis-picker-svg";
@@ -196,11 +198,9 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
item.setAttribute("tooltiptext", "");
item.style.listStyleImage = `url(${this.getSVGURL(icon)})`;
item.setAttribute("icon", icon);
item.addEventListener("command", () => {
this.#selectEmoji(this.getSVGURL(icon));
});
svgList.appendChild(item);
icons.appendChild(item);
}
this.svgList.replaceChildren(icons);
}
#onPopupShown(event) {
@@ -220,10 +220,8 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
}
this.#clearEmojis();
const emojiList = this.emojiList;
emojiList.innerHTML = "";
this.svgList.innerHTML = "";
this.emojiList.replaceChildren();
this.svgList.replaceChildren();
if (!this.#hasSelection) {
this.#currentPromiseReject?.(
@@ -9,6 +9,7 @@
overflow: clip;
:root:not([zen-no-padding="true"]) &:not(.zen-glance-overlay) {
box-shadow: var(--zen-big-shadow);
/* stylelint-disable-next-line property-no-unknown */
corner-shape: round;
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
+2 -1
View File
@@ -657,6 +657,7 @@
--urlbarview-row-padding-block: 12px;
--urlbarView-row-gutter: 0px;
border-block: 2px solid transparent;
color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important;
&:hover,
@@ -698,7 +699,7 @@
}
#urlbar-results {
max-height: 244px;
max-height: 259px;
overflow-y: auto;
scrollbar-width: none;
margin-block-start: calc(var(--urlbarView-results-padding) - 2px);
+61 -14
View File
@@ -1597,6 +1597,54 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
return true;
}
/**
* Stops the animation running on an element and pins the value it had
* reached as an inline style.
*
* @param {Element} item - The element to settle.
* @param {string} prop - The camel cased property to pin.
* @returns {string|undefined} The value the property was left at, or
* undefined if nothing was animating it.
*/
#settleAnimation(item, prop) {
const animation = this.#itemAnimations.get(item);
if (
animation?.playState !== "running" &&
animation?.playState !== "paused"
) {
return undefined;
}
const value = window.getComputedStyle(item)[prop];
animation.cancel();
item.style[prop] = value;
return value;
}
/**
* Measures how far up the group container has to be shifted to be fully
* collapsed, ignoring any collapse offset already applied to it.
*
* @param {Element} tabsContainer - The container to measure.
* @param {Element} groupStart - The element carrying the collapse offset.
* @returns {number} The height to shift the container by.
*/
#measureCollapsedShift(tabsContainer, groupStart) {
const currentMargin = groupStart.style.marginTop;
if (!parseFloat(currentMargin)) {
// The container is not shifted, it already measures its full height.
return this.#calculateHeightShift(tabsContainer, []);
}
groupStart.style.marginTop = "0px";
// Note that these have to flush, the margin has just been changed.
let heightShift = tabsContainer.getBoundingClientRect().height;
if (tabsContainer.separatorElement) {
heightShift -=
tabsContainer.separatorElement.getBoundingClientRect().height;
}
groupStart.style.marginTop = currentMargin;
return heightShift;
}
#calculateHeightShift(tabsContainer, selectedTabs) {
let heightShift = 0;
if (selectedTabs.length) {
@@ -1641,10 +1689,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (selectedTabs.length) {
tabsContainer.removeAttribute("hidden");
}
const collapsedHeight = this.#calculateHeightShift(
tabsContainer,
selectedTabs
);
const startMargin = this.#settleAnimation(groupStart, "marginTop");
const collapsedHeight = selectedTabs.length
? 0
: this.#measureCollapsedShift(tabsContainer, groupStart);
if (selectedTabs.length) {
for (let i = 0; i < groupItems.length; i++) {
@@ -1697,10 +1745,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
...this.#createAnimation(
groupStart,
{
marginTop: -(
collapsedHeight +
4 * (selectedTabs.length === 0 ? 1 : 0)
),
marginTop: [
startMargin,
-(collapsedHeight + 4 * (selectedTabs.length === 0 ? 1 : 0)),
],
},
{ duration, ease: "easeInOut" }
)
@@ -1709,16 +1757,15 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
gBrowser.tabContainer._invalidateCachedVisibleTabs();
this.#animationCount += 1;
await Promise.all(animations);
if (this.#animationCount) {
this.#animationCount -= 1;
this.#animationCount -= 1;
if (this.#animationCount || !group.collapsed) {
return;
}
// Prevent hiding if we spam the group animations
if (!selectedTabs.length && !this.#animationCount) {
if (!selectedTabs.length) {
tabsContainer.setAttribute("hidden", true);
this.styleCleanup(itemsToHide);
this.#queueCollapsedRelayout(group);
}
this.styleCleanup(itemsToHide);
}
async animateExpand(group) {
+45 -6
View File
@@ -235,7 +235,31 @@ class ZenLibraryDownloadStack {
.finally(() => download.target.refresh());
}
/**
* @param {object} download
* @returns {boolean} Whether it will be opened as soon as it is here
*/
#opensWhenDone(download) {
return !download.stopped && !!download.launchWhenSucceeded;
}
/**
* Clicking a download that is still coming in asks for it to be opened as
* soon as it is here, the way the downloads panel does.
*
* @param {object} download
*/
#toggleOpenWhenDone(download) {
download.launchWhenSucceeded = !download.launchWhenSucceeded;
download._launchedFromPanel = download.launchWhenSucceeded;
this.#updateList();
}
#openDownload(download) {
if (!download.stopped) {
this.#toggleOpenWhenDone(download);
return;
}
if (download.succeeded) {
lazy.DownloadsCommon.openDownload(download).catch(console.error);
} else if (download.source?.url) {
@@ -274,14 +298,29 @@ class ZenLibraryDownloadStack {
);
entry.querySelector(".zen-library-download-list-title").textContent =
this.#fileName(download);
entry.querySelector(".zen-library-download-list-subtitle").textContent =
this.#statusText(download);
const subtitle = entry.querySelector(
".zen-library-download-list-subtitle"
);
if (this.#opensWhenDone(download)) {
this.#window.document.l10n.setAttributes(
subtitle,
"library-downloads-open-when-done"
);
} else {
subtitle.removeAttribute("data-l10n-id");
subtitle.textContent = this.#statusText(download);
}
entry.toggleAttribute("downloading", !download.stopped);
entry.toggleAttribute("open-when-done", this.#opensWhenDone(download));
});
this.#tabs?.style.setProperty(
"--zen-library-stack-height",
`${this.#list.getBoundingClientRect().height}px`
);
this.#window
.promiseDocumentFlushed(() => this.#list.getBoundingClientRect().height)
.then(height => {
this.#tabs?.style.setProperty(
"--zen-library-stack-height",
`${height}px`
);
});
}
#updateBadge(badge, download) {
@@ -326,7 +326,34 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
lazy.DownloadsCommon.showDownloadedFile(this.#file(download));
}
/**
* Whether a download that is still coming in will be opened the moment it
* finishes.
*
* @param {object} download
* @returns {boolean}
*/
#opensWhenDone(download) {
return !download.stopped && !!download.launchWhenSucceeded;
}
/**
* Clicking a download that is still coming in asks for it to be opened as
* soon as it is here, the way the downloads panel does.
*
* @param {object} download
*/
#toggleOpenWhenDone(download) {
download.launchWhenSucceeded = !download.launchWhenSucceeded;
download._launchedFromPanel = download.launchWhenSucceeded;
this.requestUpdate();
}
#onRowClick(download) {
if (!download.stopped) {
this.#toggleOpenWhenDone(download);
return;
}
if (!this.#hasFile(download)) {
return;
}
@@ -516,15 +543,22 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
// Rendering
#renderSubtitle(download) {
const statusNode =
download === this.#openingDownload
? html`<span
class="zen-library-download-status"
data-l10n-id="library-downloads-opening-in"
></span>`
: html`<span class="zen-library-download-status"
>${this.#statusText(download)}</span
>`;
let statusNode;
if (download === this.#openingDownload) {
statusNode = html`<span
class="zen-library-download-status"
data-l10n-id="library-downloads-opening-in"
></span>`;
} else if (this.#opensWhenDone(download)) {
statusNode = html`<span
class="zen-library-download-status"
data-l10n-id="library-downloads-open-when-done"
></span>`;
} else {
statusNode = html`<span class="zen-library-download-status"
>${this.#statusText(download)}</span
>`;
}
return html`
<span class="zen-library-row-subtitle">
${statusNode}
@@ -584,6 +618,7 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
?pending=${pending}
?indeterminate=${pending && !download.hasProgress}
?paused=${pending && download.stopped}
?open-when-done=${this.#opensWhenDone(download)}
?opening=${download === this.#openingDownload}
@click=${() => this.#onRowClick(download)}
@contextmenu=${event => {
+5
View File
@@ -882,6 +882,11 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
display: none;
}
.zen-library-row[pending][open-when-done]:not(:hover)
.zen-library-download-status {
display: inline;
}
.zen-library-row[pending]:not(:hover) .zen-library-row-subtitle::before {
content: "";
display: block;
@@ -1006,6 +1006,9 @@ export class nsZenSessionManager {
aWindow.gZenWorkspaces.restoreWorkspacesFromSessionStore({
spaces: this.#sidebarWithoutCloning.spaces || [],
});
// There is no restore coming for this window, so nothing else is going to
// tell the spaces their tabs are in place.
aWindow.gZenWorkspaces.onWindowRestored();
}
/**
+4 -4
View File
@@ -133,11 +133,11 @@ class nsZenWorkspaceIcons extends MozXULElement {
async #updateIcons() {
const workspaces = gZenWorkspaces.getWorkspaces();
this.innerHTML = "";
const icons = document.createDocumentFragment();
for (const workspace of workspaces) {
const button = this.#createWorkspaceIcon(workspace);
this.appendChild(button);
icons.appendChild(this.#createWorkspaceIcon(workspace));
}
this.replaceChildren(icons);
if (workspaces.length <= 1) {
this.setAttribute("dont-show", "true");
} else {
@@ -169,7 +169,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
for (const button of buttons) {
if (button.getAttribute("zen-workspace-id") == uuid) {
selected = i;
} else {
} else if (button.hasAttribute("active")) {
button.removeAttribute("active");
}
i++;
+178 -72
View File
@@ -76,16 +76,6 @@ class nsZenWorkspaces {
this._resolveInitialized = resolve;
});
async #waitForPromises() {
if (this.privateWindowOrDisabled) {
return;
}
await Promise.all([
this.promisePinnedInitialized,
SessionStore.promiseAllWindowsRestored,
]);
}
async init() {
// Initialize workspace change mutex
this._workspaceChangeInProgress = false;
@@ -152,8 +142,9 @@ class nsZenWorkspaces {
this.addPopupListeners();
if (this.privateWindowOrDisabled) {
await this.#waitForPromises();
await this.restoreWorkspacesFromSessionStore({});
await delayedStartupPromise;
this.restoreWorkspacesFromSessionStore({});
await this.onWindowRestored();
}
if (!this.privateWindowOrDisabled) {
@@ -300,6 +291,9 @@ class nsZenWorkspaces {
inBackground: true,
userContextId: 0,
_forZenEmptyTab: true,
skipAnimation: true,
skipBackgroundNotify: true,
bulkOrderedOpen: true,
});
}
@@ -371,39 +365,27 @@ class nsZenWorkspaces {
"tabbrowser-arrowscrollbox-periphery"
);
perifery.setAttribute("hidden", "true");
const tabs = gBrowser.tabContainer.allTabs;
const markupTabs = gBrowser.tabContainer.allTabs;
const workspaces = this.getWorkspaces();
const spaceElements = document.createDocumentFragment();
for (const workspace of workspaces) {
this.#createWorkspaceTabsSection(workspace, tabs);
spaceElements.appendChild(this.#createWorkspaceElement(workspace));
}
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
);
}
}
document
.getElementById("tabbrowser-arrowscrollbox")
.appendChild(spaceElements);
this._hasInitializedTabsStrip = true;
for (const tab of markupTabs) {
// Routes into the active space's strip now that one exists.
gBrowser.tabContainer.appendChild(tab);
}
if (markupTabs.length) {
gBrowser.tabContainer._invalidateCachedTabs();
}
for (const workspace of workspaces) {
this.workspaceElement(workspace.uuid)?.checkPinsExistence();
}
perifery.setAttribute("hidden", "true");
this._hasInitializedTabsStrip = true;
this._fixIndicatorsNames(workspaces);
}
@@ -455,15 +437,20 @@ class nsZenWorkspaces {
return this.getEssentialsSection(this.getCurrentSpaceContainerId());
}
#createWorkspaceTabsSection(workspace, tabs = []) {
#createWorkspaceElement(workspace) {
const workspaceWrapper = document.createXULElement("zen-workspace");
const container = document.getElementById("tabbrowser-arrowscrollbox");
workspaceWrapper.id = workspace.uuid;
if (this.activeWorkspace === workspace.uuid) {
workspaceWrapper.active = true;
}
return workspaceWrapper;
}
container.appendChild(workspaceWrapper);
#createWorkspaceTabsSection(workspace, tabs = []) {
const workspaceWrapper = this.#createWorkspaceElement(workspace);
document
.getElementById("tabbrowser-arrowscrollbox")
.appendChild(workspaceWrapper);
this.#organizeTabsToWorkspaceSections(workspace, workspaceWrapper, tabs);
workspaceWrapper.checkPinsExistence();
}
@@ -512,6 +499,85 @@ class nsZenWorkspaces {
}
}
/** @type {Map<Element, DocumentFragment>|null} */
#sessionRestoreFragments = null;
get canCollectSessionRestoreFragments() {
return (
this._hasInitializedTabsStrip &&
!!this.activeWorkspaceElement?.tabsContainer
);
}
beginSessionRestoreFragments() {
this.#sessionRestoreFragments = new Map();
}
/**
* @param {object} aTabData
* Session store data of the tab, or of the first tab of the folder,
* that is about to be inserted.
* @returns {DocumentFragment} The fragment for the section it belongs to.
*/
getSessionRestoreFragment(aTabData) {
const container = this.#sessionRestoreContainer(aTabData);
let fragment = this.#sessionRestoreFragments.get(container);
if (!fragment) {
fragment = document.createDocumentFragment();
this.#sessionRestoreFragments.set(container, fragment);
}
return fragment;
}
/**
* Inserts every collected fragment into its section, one insertion per
* section instead of one move per restored tab.
*/
flushSessionRestoreFragments() {
if (!this.#sessionRestoreFragments) {
return;
}
const filledSpaces = new Set();
for (const [container, fragment] of this.#sessionRestoreFragments) {
container.insertBefore(fragment, this.#sessionRestoreAnchor(container));
const spaceElement = container.closest("zen-workspace");
if (spaceElement) {
filledSpaces.add(spaceElement);
}
}
this.#sessionRestoreFragments = null;
for (const spaceElement of filledSpaces) {
spaceElement.checkPinsExistence();
}
gBrowser.tabContainer._invalidateCachedTabs();
}
#sessionRestoreContainer(aTabData) {
if (aTabData.zenEssential) {
return this.getEssentialsSection(aTabData.userContextId);
}
let spaceElement = this.workspaceElement(aTabData.zenWorkspace);
if (!spaceElement?.tabsContainer) {
// The space is gone, or the tab never had one. Keep it in the active
// space, #clearAnyZombieTabs takes care of it later on if need be.
spaceElement = this.activeWorkspaceElement;
}
return aTabData.pinned
? spaceElement.pinnedTabsContainer
: spaceElement.tabsContainer;
}
#sessionRestoreAnchor(aContainer) {
// Essentials are shared between spaces, so a restore only ever adds to
// what is already there. Every other section gets the restored tabs in
// front of whatever the window started with, which is the tab the startup
// page opened with and which belongs at the end.
if (aContainer.classList.contains("zen-essentials-container")) {
return null;
}
return aContainer.firstChild;
}
initializeWorkspaceNavigation() {
this.#setupAppCommandHandlers();
this.#setupSidebarHandlers();
@@ -796,41 +862,68 @@ class nsZenWorkspaces {
console.error("gZenWorkspaces: Error initializing theme picker", e);
}
this.#initializeTabsStripSections();
return this.promiseInitialized;
}
#hasFinishedInitialization = false;
async onWindowRestored() {
if (this.#hasFinishedInitialization || !this.workspaceEnabled) {
return;
}
this.#hasFinishedInitialization = true;
const activeWorkspace = this.getActiveWorkspace();
if (!this.privateWindowOrDisabled) {
await this.promisePinnedInitialized;
}
this.#initializeEmptyTab();
return (async () => {
await this.#waitForPromises();
this.#afterLoadInit();
await this.#initializeWorkspaceBookmarks();
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
this._resolveInitialized();
this.#clearAnyZombieTabs(); // Dont call with await
delete this._resolveInitialized;
this.#afterLoadInit();
await this.#initializeWorkspaceBookmarks();
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
// The spaces hold their tabs now, so the tab the user is waiting for can
// be selected without waiting on anything else.
try {
await this.selectStartPage();
} catch (e) {
console.error("gZenWorkspaces: Error selecting the start page", e);
}
this._resolveInitialized();
this.#clearAnyZombieTabs(); // Dont call with await
delete this._resolveInitialized;
const tabUpdateListener = this.updateTabsContainers.bind(this);
window.addEventListener("TabOpen", tabUpdateListener);
window.addEventListener("TabClose", tabUpdateListener);
window.addEventListener("TabAddedToEssentials", tabUpdateListener);
window.addEventListener("TabRemovedFromEssentials", tabUpdateListener);
window.addEventListener("TabPinned", tabUpdateListener);
window.addEventListener("TabUnpinned", tabUpdateListener);
window.addEventListener("aftercustomization", tabUpdateListener);
window.addEventListener("TabSelect", this.onLocationChange.bind(this));
window.addEventListener(
"TabBrowserInserted",
this.onTabBrowserInserted.bind(this)
);
const tabUpdateListener = this.updateTabsContainers.bind(this);
window.addEventListener("TabOpen", tabUpdateListener);
window.addEventListener("TabClose", tabUpdateListener);
window.addEventListener("TabAddedToEssentials", tabUpdateListener);
window.addEventListener("TabRemovedFromEssentials", tabUpdateListener);
window.addEventListener("TabPinned", tabUpdateListener);
window.addEventListener("TabUnpinned", tabUpdateListener);
window.addEventListener("aftercustomization", tabUpdateListener);
window.addEventListener("TabSelect", this.onLocationChange.bind(this));
window.addEventListener(
"TabBrowserInserted",
this.onTabBrowserInserted.bind(this)
);
this.updateWorkspacesChangeContextMenu();
})();
this.updateWorkspacesChangeContextMenu();
}
async selectStartPage() {
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
if (
!this.workspaceEnabled ||
gZenUIManager.testingEnabled ||
this.#hasSelectedStartPage
) {
return;
}
await this.promiseInitialized;
// #hasInitialized only goes up once the whole init promise settles, so a
// second restore arriving before that would otherwise run this again.
this.#hasSelectedStartPage = true;
// _handleURIToLoad selects the tab it loads the startup URI into, so
// picking ours before delayed startup is done just gets overwritten.
await delayedStartupPromise;
let resolveSelectPromise;
let selectPromise = new Promise(resolve => {
resolveSelectPromise = resolve;
@@ -921,8 +1014,12 @@ class nsZenWorkspaces {
// Wait for the next event loop to ensure that the startup focus logic by
// firefox has finished doing it's thing.
setTimeout(() => {
if (document.documentElement.hasAttribute("zen-welcome-stage")) {
const focusTimer = setTimeout(() => {
if (
window.closed ||
!window.docShell ||
document.documentElement.hasAttribute("zen-welcome-stage")
) {
return;
}
if (gZenVerticalTabsManager._canReplaceNewTab && shownEmptyTab) {
@@ -933,6 +1030,11 @@ class nsZenWorkspaces {
gBrowser.selectedBrowser.focus();
}
});
// A window torn down before this runs would leave the focus work in
// flight against a destroyed urlbar.
window.addEventListener("unload", () => clearTimeout(focusTimer), {
once: true,
});
if (
!gZenVerticalTabsManager._canReplaceNewTab &&
@@ -961,6 +1063,8 @@ class nsZenWorkspaces {
);
}
#hasSelectedStartPage = false;
handleInitialTab(tab, isEmpty) {
if (gZenUIManager.testingEnabled || !this.workspaceEnabled) {
return;
@@ -2552,7 +2656,9 @@ class nsZenWorkspaces {
#createWorkspaceData(name, icon, containerTabId = 0) {
if (!this.currentWindowIsSyncing) {
containerTabId =
parseInt(gBrowser.selectedTab.getAttribute("usercontextid")) || 0;
parseInt(
globalThis.gBrowser?.selectedTab?.getAttribute("usercontextid")
) || 0;
let label =
ContextualIdentityService.getUserContextLabel(containerTabId) ||
"Default";
-5
View File
@@ -145,10 +145,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return;
}
const tab = event.target;
if (this._ignoreNextTabPinnedEvent) {
delete this._ignoreNextTabPinnedEvent;
return;
}
switch (action) {
case "TabPinned":
tab._zenClickEventListener = this._zenClickEventListener;
@@ -558,7 +554,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
});
} else {
gBrowser.pinTab(tab);
this._ignoreNextTabPinnedEvent = true;
}
tab.setAttribute("zenDefaultUserContextId", true);
if (tab.selected) {
+2
View File
@@ -20,6 +20,8 @@ support-files = [
["browser_folder_issue_9981.js"]
["browser_folder_issue_15522.js"]
["browser_folder_level_checks.js"]
["browser_folder_max_subfolders.js"]
@@ -0,0 +1,64 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TOGGLE_DELAY = 90;
function folderHeight(folder) {
return folder.getBoundingClientRect().height;
}
async function waitForFolderSettled(folder, message) {
let previous = null;
await TestUtils.waitForCondition(() => {
const height = folderHeight(folder);
const settled = previous !== null && Math.abs(height - previous) < 1;
previous = height;
return settled;
}, message);
}
add_task(async function test_Issue_15522() {
const tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
const tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");
const folder = await gZenFolders.createFolder([tab1, tab2], {
renameFolder: false,
label: "folder",
});
await waitForFolderSettled(folder, "Folder should be done opening");
const expandedHeight = folderHeight(folder);
folder.collapsed = true;
await waitForFolderSettled(folder, "Folder should be done collapsing");
const collapsedHeight = folderHeight(folder);
Assert.less(
collapsedHeight,
expandedHeight,
"Collapsing the folder should hide its tabs"
);
folder.collapsed = false;
await waitForFolderSettled(folder, "Folder should be done expanding");
Assert.lessOrEqual(
Math.abs(folderHeight(folder) - expandedHeight),
1,
"Expanding the folder should show its tabs again"
);
folder.collapsed = true;
await new Promise(resolve => setTimeout(resolve, TOGGLE_DELAY));
folder.collapsed = false;
await new Promise(resolve => setTimeout(resolve, TOGGLE_DELAY));
folder.collapsed = true;
await waitForFolderSettled(folder, "Folder should be done collapsing");
ok(folder.collapsed, "Folder should be collapsed after spamming toggles");
Assert.lessOrEqual(
Math.abs(folderHeight(folder) - collapsedHeight),
1,
"Folder should be fully collapsed instead of stuck half open"
);
await removeFolder(folder);
});
@@ -5,6 +5,8 @@
ChromeUtils.defineESModuleGetters(this, {
globalActions: "resource:///modules/ZenUBGlobalActions.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
});
@@ -37,3 +39,29 @@ add_task(async function test_Ub_Actions_Search() {
);
}
});
add_task(async function test_Ub_Actions_Exact_Match_Ranks_First() {
// With a closed tab in history, "Reopen Closed Tab" is available too, and it
// contains "close tab". The exact label must still be the first action.
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com"
);
await TabStateFlusher.flush(tab.linkedBrowser);
BrowserTestUtils.removeTab(tab);
await TestUtils.waitForCondition(
() => SessionStore.getClosedTabCount(window) > 0,
"Waiting for the closed tab to be recorded"
);
const closeTab = globalActions.find(a => a.command === "cmd_close");
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
waitForFocus,
value: closeTab.label,
});
let { result } = await UrlbarTestUtils.getRowAt(window, 1);
Assert.equal(result.providerName, "ZenUrlbarProviderGlobalActions");
Assert.equal(result.payload.title, closeTab.label);
SessionStore.forgetClosedTab(window, 0);
});
+9 -2
View File
@@ -18,6 +18,13 @@ const MAX_RECENT_ACTIONS = 5;
const MINIMUM_QUERY_SCORE = 92;
const MINIMUM_PREFIXED_QUERY_SCORE = 30;
// Exact and prefix matches must outrank any fuzzy match. The fuzzy score grows
// quadratically with a run of consecutive matches, so a longer label that merely
// contains the query ("Reopen Closed Tab" for "close tab") could beat the label
// that is exactly the query.
const EXACT_MATCH_SCORE = 1_000_000;
const PREFIX_MATCH_SCORE = 100_000;
ChromeUtils.defineESModuleGetters(lazy, {
UrlbarResult: "chrome://browser/content/urlbar/UrlbarResult.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
@@ -298,11 +305,11 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
}
// 1. Exact match gets the highest score.
if (targetLower === queryLower) {
return 200;
return EXACT_MATCH_SCORE;
}
// 2. Exact prefix matches are heavily prioritized.
if (targetLower.startsWith(queryLower)) {
return 100 + queryLen;
return PREFIX_MATCH_SCORE + queryLen;
}
let score = 0;
let queryIndex = 0;