mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-30 18:31:30 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e89bd7796e | ||
|
|
66c5931a29 | ||
|
|
a5585c5619 | ||
|
|
2326d2b56b | ||
|
|
9ef1d96c00 | ||
|
|
432f93770f | ||
|
|
f28e0688fb | ||
|
|
8df6fef1ac | ||
|
|
4746d36699 | ||
|
|
9e0c790602 | ||
|
|
3d53b37e8b | ||
|
|
e7ddfb52db | ||
|
|
ba70449bdd | ||
|
|
bf40c31149 | ||
|
|
7a093beae4 | ||
|
|
03b4e934fe | ||
|
|
b93d19b873 | ||
|
|
b9c4000fe2 | ||
|
|
0b7687ec6d | ||
|
|
d70b3d902c | ||
|
|
f9f8f2f372 | ||
|
|
99dca42723 | ||
|
|
ed0a6fd447 | ||
|
|
88c6611f1e | ||
|
|
e66e763e63 | ||
|
|
721135f576 | ||
|
|
d8a934d2ec | ||
|
|
91a1d4d2ca | ||
|
|
97577dac26 | ||
|
|
5723a89bcd |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -5,4 +5,4 @@
|
||||
*.patch linguist-language=C++
|
||||
*.d.ts linguist-language=TypeScript
|
||||
|
||||
src/zen/tests/*.js linguist-language=Test
|
||||
src/zen/tests/** linguist-language=C++
|
||||
|
||||
2
.github/workflows/linux-release-build.yml
vendored
2
.github/workflows/linux-release-build.yml
vendored
@@ -105,6 +105,8 @@ jobs:
|
||||
SURFER_COMPAT: ${{ matrix.arch }}
|
||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
npm run import
|
||||
|
||||
2
.github/workflows/macos-release-build.yml
vendored
2
.github/workflows/macos-release-build.yml
vendored
@@ -122,6 +122,8 @@ jobs:
|
||||
SURFER_COMPAT: ${{ matrix.arch }}
|
||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||
SURFER_PLATFORM: darwin
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
2
.github/workflows/windows-release-build.yml
vendored
2
.github/workflows/windows-release-build.yml
vendored
@@ -166,6 +166,8 @@ jobs:
|
||||
SURFER_COMPAT: ${{ matrix.arch }}
|
||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
npm run import -- --verbose
|
||||
|
||||
@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
|
||||
|
||||
### Firefox Versions
|
||||
|
||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0`! 🚀
|
||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0`!
|
||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0.1`!
|
||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 155.0`!
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1fb0703fa9723fd9c400ad60d18e64ee4f9bf125
|
||||
6b4abe0093721c1be89eb41b3fb060dd6336923d
|
||||
@@ -3,8 +3,6 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
param(
|
||||
[string][Parameter(Mandatory=$true)]$SignIdentity,
|
||||
[string][Parameter(Mandatory=$true)]$SignIdentityIssuer,
|
||||
[string][Parameter(Mandatory=$true)]$GithubRunId
|
||||
)
|
||||
|
||||
@@ -24,11 +22,15 @@ mkdir windsign-temp -ErrorAction SilentlyContinue
|
||||
# echo "Downloaded git objects repo to"
|
||||
#} -Verbose -ArgumentList $PWD -Debug
|
||||
|
||||
$token = gh auth token
|
||||
|
||||
$env:SURFER_MOZCONFIG_ONLY="1"
|
||||
$env:SURFER_SIGNING_MODE=""
|
||||
|
||||
$env:SURFER_CERT_PATCH_ISSUER=$SignIdentityIssuer
|
||||
$env:SURFER_CERT_PATCH_NAME=$SignIdentity
|
||||
get-content "$PSScriptRoot/../.env" | foreach {
|
||||
$name, $value = $_.split('=')
|
||||
set-content env:\$name $value
|
||||
}
|
||||
|
||||
Start-Job -Name "DownloadGitl10n" -ScriptBlock {
|
||||
param($PWD)
|
||||
@@ -46,7 +48,6 @@ npm run build
|
||||
|
||||
echo "Downloading artifacts info"
|
||||
$artifactsInfo=gh api repos/zen-browser/desktop/actions/runs/$GithubRunId/artifacts
|
||||
$token = gh auth token
|
||||
|
||||
function New-TemporaryDirectory {
|
||||
$tmp = [System.IO.Path]::GetTempPath() # Not $env:TEMP, see https://stackoverflow.com/a/946017
|
||||
|
||||
@@ -164,5 +164,5 @@ zen-window-sync-migration-dialog-learn-more = Learn More
|
||||
zen-window-sync-migration-dialog-accept = Got It
|
||||
|
||||
zen-appmenu-new-blank-window =
|
||||
.label = New blank window
|
||||
.label = New Blank Window
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
tab-zen-split-tabs =
|
||||
.label =
|
||||
{ $tabCount ->
|
||||
[-1] Split out tab
|
||||
[1] Add split view...
|
||||
[-1] Split Out Tab
|
||||
[1] Add Split View...
|
||||
*[other] Join { $tabCount } Tabs
|
||||
}
|
||||
.accesskey = S
|
||||
|
||||
zen-split-link =
|
||||
.label = Split link to new tab
|
||||
.label = Split Link to New Tab
|
||||
.accesskey = S
|
||||
|
||||
zen-split-view-modifier-header = Split View
|
||||
|
||||
@@ -86,6 +86,8 @@ zen-workspace-default-profile = Default
|
||||
|
||||
zen-workspaces-delete-workspace-title = Delete Space?
|
||||
zen-workspaces-delete-workspace-body = Are you sure you want to delete { $name }? This action cannot be undone.
|
||||
zen-workspaces-remote-delete-title = Delete Synced Space?
|
||||
zen-workspaces-remote-delete-body = { $name } was deleted on another device. Delete it here too? Keeping it will restore it on your other devices.
|
||||
|
||||
# Note that the html tag MUST not be changed or removed, as it is used to better
|
||||
# display the shortcut in the toast notification.
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@zen-browser/prettier": "^3.9.3",
|
||||
"@zen-browser/surfer": "^1.14.7",
|
||||
"@zen-browser/surfer": "^1.14.8",
|
||||
"formal-git": "^1.2.9",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
@@ -860,9 +860,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@zen-browser/surfer": {
|
||||
"version": "1.14.7",
|
||||
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.7.tgz",
|
||||
"integrity": "sha512-Ui2Chqvr4/+FmHM54eKOv7cQu8DRAK/uCX9o1GNi81hszxo/RSQwaw71eNW43Qcj7giK4U4x0IdCMht98iQ0rA==",
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.8.tgz",
|
||||
"integrity": "sha512-5HHcfibwHIK5n3huhJL2TLBX2+8odj6ebCMA6U2EcUj9ROSrcz7/bjY2n5MIa9oIRL9gtV3ott0jwJj6SlapxA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@zen-browser/prettier": "^3.9.3",
|
||||
"@zen-browser/surfer": "^1.14.7",
|
||||
"@zen-browser/surfer": "^1.14.8",
|
||||
"formal-git": "^1.2.9",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
- name: browser.search.widget.new
|
||||
value: true
|
||||
|
||||
- name: layout.css.corner-shape.enabled
|
||||
value: true
|
||||
|
||||
# Disabled from https://searchfox.org/firefox-main/rev/d6bfff43852356ca98af848b4705d37f8d41856f/modules/libpref/init/StaticPrefList.yaml#2008
|
||||
# Only enabled for windows, doesn't really fit inside Zen.
|
||||
- name: browser.startup.preXulSkeletonUI
|
||||
|
||||
@@ -10,3 +10,6 @@
|
||||
value: false
|
||||
locked: true
|
||||
condition: "!@IS_TWILIGHT@"
|
||||
|
||||
- name: zen.spaces-sync.debug
|
||||
value: false
|
||||
|
||||
@@ -8,4 +8,4 @@ packaging==24.2
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
pycodestyle==2.12.1
|
||||
requests==2.33.0
|
||||
requests==2.34.2
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/.prettierignore b/.prettierignore
|
||||
index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073afaebeb8a 100644
|
||||
index bd8ab46ff7175f5039b69b04c1afe53ab8efe746..7ae12a1ff3724e99aeec2e93518640a2fa9611e6 100644
|
||||
--- a/.prettierignore
|
||||
+++ b/.prettierignore
|
||||
@@ -1803,3 +1803,12 @@ tools/ts/test/baselines/
|
||||
@@ -1807,3 +1807,12 @@ tools/ts/test/baselines/
|
||||
try_task_config.json
|
||||
xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
|
||||
**/package-lock.json
|
||||
@@ -12,6 +12,6 @@ index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073a
|
||||
+*.min.js
|
||||
+*.min.mjs
|
||||
+*.inc
|
||||
+*/mochitests/*
|
||||
+**/mochitests/**
|
||||
+*.svg
|
||||
+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/.stylelintrc.js b/.stylelintrc.js
|
||||
index b484bf6600a7e1b8ad2aed8c8b7fe5e84cc3d023..f89712e2c13dfa0bcb48e55cd3872c67f5c98dad 100644
|
||||
index b5f8245fa04ffb8fc82240261d63f893f4629f19..1b6ead24adbe69fcb5aeb95cfd3e7fa9662d89d8 100644
|
||||
--- a/.stylelintrc.js
|
||||
+++ b/.stylelintrc.js
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
@@ -11,10 +11,10 @@ index b484bf6600a7e1b8ad2aed8c8b7fe5e84cc3d023..f89712e2c13dfa0bcb48e55cd3872c67
|
||||
{
|
||||
ignore: ["blockless-at-rules"],
|
||||
},
|
||||
@@ -280,7 +280,7 @@ module.exports = {
|
||||
"media-query-no-invalid": null,
|
||||
@@ -281,7 +281,7 @@ module.exports = {
|
||||
"stylelint-plugin-mozilla/media-query-no-invalid": true,
|
||||
"stylelint-plugin-mozilla/no-base-design-tokens": true,
|
||||
"stylelint-plugin-mozilla/no-has-selector": true,
|
||||
- "stylelint-plugin-mozilla/use-design-tokens": true,
|
||||
+ "stylelint-plugin-mozilla/use-design-tokens": false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
index dd2f391096968564086911c532f696dc7ffb8ae1..3de617bf4fb7fa35f7d4d7b618e4673b72b953ee 100644
|
||||
index dd2f391096968564086911c532f696dc7ffb8ae1..c968e5f0ebb5f0151fb124bafe10d8d5361e060f 100644
|
||||
--- a/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
@@ -2,7 +2,7 @@
|
||||
@@ -19,7 +19,7 @@ index dd2f391096968564086911c532f696dc7ffb8ae1..3de617bf4fb7fa35f7d4d7b618e4673b
|
||||
</toolbar>
|
||||
|
||||
+<hbox id="titlebar">
|
||||
+ <html:div id="zen-toolbar-background" class="zen-toolbar-background zen-browser-generic-background">
|
||||
+ <html:div id="zen-toolbar-background" class="zen-toolbar-background zen-squircle-before zen-squircle-after zen-browser-generic-background">
|
||||
+ <html:div class="zen-browser-grain" />
|
||||
+ </html:div>
|
||||
+ <box id="zen-overflow-extensions-list" skipintoolbarset="true" contextmenu="toolbar-context-menu" />
|
||||
|
||||
@@ -15,19 +15,23 @@
|
||||
mainview-with-header="true">
|
||||
<hbox id="zen-site-data-header">
|
||||
<toolbarbutton id="zen-site-data-header-share"
|
||||
class="zen-squircle-before"
|
||||
data-l10n-id="zen-site-data-header-share"
|
||||
flex="1"
|
||||
closemenu="none" />
|
||||
<toolbarbutton id="zen-site-data-header-reader-mode"
|
||||
class="zen-squircle-before"
|
||||
data-l10n-id="zen-site-data-header-reader-mode"
|
||||
command="View:ReaderView"
|
||||
tooltip="dynamic-shortcut-tooltip"
|
||||
flex="1" />
|
||||
<toolbarbutton id="zen-site-data-header-screenshot"
|
||||
class="zen-squircle-before"
|
||||
data-l10n-id="zen-site-data-header-screenshot"
|
||||
command="Browser:Screenshot"
|
||||
flex="1" />
|
||||
<toolbarbutton id="zen-site-data-header-bookmark"
|
||||
class="zen-squircle-before"
|
||||
data-l10n-id="zen-site-data-header-bookmark"
|
||||
command="Browser:AddBookmarkAs"
|
||||
flex="1" />
|
||||
|
||||
@@ -131,10 +131,10 @@
|
||||
</box>
|
||||
<html:input type="range" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
|
||||
#ifdef XP_MACOSX
|
||||
max="0.9"
|
||||
max="0.8"
|
||||
min="0.30"
|
||||
#else
|
||||
max="0.9"
|
||||
max="0.8"
|
||||
min="0.25"
|
||||
#endif
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs
|
||||
index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2681652e5 100644
|
||||
index 6d1885a54815d10b3a4e79f7e5acd61ab3abb159..97e07d85c69ec97baeab05aedd57bd8869254d59 100644
|
||||
--- a/browser/components/customizableui/CustomizableUI.sys.mjs
|
||||
+++ b/browser/components/customizableui/CustomizableUI.sys.mjs
|
||||
@@ -13,6 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
@@ -10,7 +10,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
HomePage: "resource:///modules/HomePage.sys.mjs",
|
||||
PanelMultiView:
|
||||
"moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
|
||||
@@ -348,7 +349,7 @@ var CustomizableUIInternal = {
|
||||
@@ -352,7 +353,7 @@ var CustomizableUIInternal = {
|
||||
{
|
||||
type: CustomizableUI.TYPE_PANEL,
|
||||
defaultPlacements: [],
|
||||
@@ -19,7 +19,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -358,27 +359,23 @@ var CustomizableUIInternal = {
|
||||
@@ -362,27 +363,23 @@ var CustomizableUIInternal = {
|
||||
"back-button",
|
||||
"forward-button",
|
||||
"stop-reload-button",
|
||||
@@ -50,17 +50,18 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
defaultCollapsed: false,
|
||||
},
|
||||
true
|
||||
@@ -402,9 +399,6 @@ var CustomizableUIInternal = {
|
||||
@@ -406,10 +403,6 @@ var CustomizableUIInternal = {
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
defaultPlacements: [
|
||||
"tabbrowser-tabs",
|
||||
- "new-tab-button",
|
||||
- "spring",
|
||||
- "alltabs-button",
|
||||
- "ai-window-toggle",
|
||||
],
|
||||
verticalTabsDefaultPlacements: [],
|
||||
defaultCollapsed: null,
|
||||
@@ -484,6 +478,7 @@ var CustomizableUIInternal = {
|
||||
@@ -502,6 +495,7 @@ var CustomizableUIInternal = {
|
||||
CustomizableUI.AREA_NAVBAR,
|
||||
CustomizableUI.AREA_BOOKMARKS,
|
||||
CustomizableUI.AREA_TABSTRIP,
|
||||
@@ -68,7 +69,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
]);
|
||||
if (AppConstants.platform != "macosx") {
|
||||
toolbars.add(CustomizableUI.AREA_MENUBAR);
|
||||
@@ -1285,6 +1280,9 @@ var CustomizableUIInternal = {
|
||||
@@ -1334,6 +1328,9 @@ var CustomizableUIInternal = {
|
||||
placements = gPlacements.get(area);
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
// For toolbars that need it, mark as dirty.
|
||||
let defaultPlacements = areaProperties.get("defaultPlacements");
|
||||
if (
|
||||
@@ -1793,7 +1791,6 @@ var CustomizableUIInternal = {
|
||||
@@ -1842,7 +1839,6 @@ var CustomizableUIInternal = {
|
||||
lazy.log.info(
|
||||
"Widget " + aWidgetId + " not found, unable to remove from " + aArea
|
||||
);
|
||||
@@ -86,7 +87,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
}
|
||||
|
||||
this.notifyDOMChange(widgetNode, null, container, true, () => {
|
||||
@@ -1803,7 +1800,7 @@ var CustomizableUIInternal = {
|
||||
@@ -1852,7 +1848,7 @@ var CustomizableUIInternal = {
|
||||
// We also need to remove the panel context menu if it's there:
|
||||
this.ensureButtonContextMenu(widgetNode);
|
||||
if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
|
||||
@@ -95,7 +96,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
} else {
|
||||
window.gNavToolbox.palette.appendChild(widgetNode);
|
||||
}
|
||||
@@ -1971,16 +1968,16 @@ var CustomizableUIInternal = {
|
||||
@@ -2020,16 +2016,16 @@ var CustomizableUIInternal = {
|
||||
elem.setAttribute("skipintoolbarset", "true");
|
||||
}
|
||||
}
|
||||
@@ -115,7 +116,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
// Handle initial state of vertical tabs.
|
||||
if (isVerticalTabs) {
|
||||
// Show the vertical tabs toolbar
|
||||
@@ -2222,6 +2219,10 @@ var CustomizableUIInternal = {
|
||||
@@ -2271,6 +2267,10 @@ var CustomizableUIInternal = {
|
||||
* The identifier string of the area that aNode is being inserted into.
|
||||
*/
|
||||
insertWidgetBefore(aNode, aNextNode, aContainer, aAreaId) {
|
||||
@@ -126,7 +127,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
this.notifyDOMChange(aNode, aNextNode, aContainer, false, () => {
|
||||
this.setLocationAttributes(aNode, aAreaId);
|
||||
aContainer.insertBefore(aNode, aNextNode);
|
||||
@@ -4592,7 +4593,7 @@ var CustomizableUIInternal = {
|
||||
@@ -4647,7 +4647,7 @@ var CustomizableUIInternal = {
|
||||
* For all registered areas, builds those areas to reflect the current
|
||||
* placement state of all widgets.
|
||||
*/
|
||||
@@ -135,7 +136,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
for (let [areaId, areaNodes] of gBuildAreas) {
|
||||
let placements = gPlacements.get(areaId);
|
||||
let isFirstChangedToolbar = true;
|
||||
@@ -4603,7 +4604,7 @@ var CustomizableUIInternal = {
|
||||
@@ -4658,7 +4658,7 @@ var CustomizableUIInternal = {
|
||||
if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
|
||||
let defaultCollapsed = area.get("defaultCollapsed");
|
||||
let win = areaNode.documentGlobal;
|
||||
@@ -144,7 +145,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
win.setToolbarVisibility(
|
||||
areaNode,
|
||||
typeof defaultCollapsed == "string"
|
||||
@@ -5899,6 +5900,7 @@ export var CustomizableUI = {
|
||||
@@ -5971,6 +5971,7 @@ export var CustomizableUI = {
|
||||
unregisterArea(aName, aDestroyPlacements) {
|
||||
CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
|
||||
},
|
||||
@@ -152,7 +153,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
/**
|
||||
* Add a widget to an area.
|
||||
* If the area to which you try to add is not known to CustomizableUI,
|
||||
@@ -7869,7 +7871,9 @@ class OverflowableToolbar {
|
||||
@@ -7946,7 +7947,9 @@ class OverflowableToolbar {
|
||||
);
|
||||
|
||||
if (webExtList && CustomizableUI.isWebExtensionWidget(child.id)) {
|
||||
@@ -162,7 +163,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
webExtList.insertBefore(child, webExtList.firstElementChild);
|
||||
} else {
|
||||
child.setAttribute("cui-anchorid", this.#defaultListButton.id);
|
||||
@@ -7929,7 +7933,7 @@ class OverflowableToolbar {
|
||||
@@ -8006,7 +8009,7 @@ class OverflowableToolbar {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -171,7 +172,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
if (child != aExceptChild) {
|
||||
sum += getInlineSize(child);
|
||||
}
|
||||
@@ -7953,11 +7957,11 @@ class OverflowableToolbar {
|
||||
@@ -8030,11 +8033,11 @@ class OverflowableToolbar {
|
||||
parseFloat(style.paddingLeft) -
|
||||
parseFloat(style.paddingRight) -
|
||||
toolbarChildrenWidth;
|
||||
@@ -185,7 +186,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
});
|
||||
|
||||
lazy.log.debug(
|
||||
@@ -7972,7 +7976,39 @@ class OverflowableToolbar {
|
||||
@@ -8049,7 +8052,39 @@ class OverflowableToolbar {
|
||||
Math.max(targetWidth, targetChildrenWidth)
|
||||
);
|
||||
totalAvailWidth = Math.ceil(totalAvailWidth);
|
||||
@@ -226,7 +227,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
return { isOverflowing, targetContentWidth, totalAvailWidth };
|
||||
}
|
||||
|
||||
@@ -8033,7 +8069,11 @@ class OverflowableToolbar {
|
||||
@@ -8110,7 +8145,11 @@ class OverflowableToolbar {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +240,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
lazy.log.debug(
|
||||
`Need ${minSize} but width is ${totalAvailWidth} so bailing`
|
||||
);
|
||||
@@ -8066,7 +8106,7 @@ class OverflowableToolbar {
|
||||
@@ -8143,7 +8182,7 @@ class OverflowableToolbar {
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
@@ -248,7 +249,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
}
|
||||
child.removeAttribute("cui-anchorid");
|
||||
child.removeAttribute("overflowedItem");
|
||||
@@ -8192,6 +8232,9 @@ class OverflowableToolbar {
|
||||
@@ -8269,6 +8308,9 @@ class OverflowableToolbar {
|
||||
* if no such list exists.
|
||||
*/
|
||||
get #webExtList() {
|
||||
@@ -258,7 +259,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
if (!this.#webExtListRef) {
|
||||
let targetID = this.#toolbar.getAttribute("addon-webext-overflowtarget");
|
||||
if (!targetID) {
|
||||
@@ -8203,6 +8246,9 @@ class OverflowableToolbar {
|
||||
@@ -8280,6 +8322,9 @@ class OverflowableToolbar {
|
||||
let win = this.#toolbar.documentGlobal;
|
||||
let { panel } = win.gUnifiedExtensions;
|
||||
this.#webExtListRef = panel.querySelector(`#${targetID}`);
|
||||
@@ -268,7 +269,7 @@ index 491161e48044f75167a8e8a0348779cfa22fc40e..867d51b8a6b74f841afc6d84dcdfd8f2
|
||||
}
|
||||
return this.#webExtListRef;
|
||||
}
|
||||
@@ -8411,7 +8457,7 @@ class OverflowableToolbar {
|
||||
@@ -8488,7 +8533,7 @@ class OverflowableToolbar {
|
||||
break;
|
||||
}
|
||||
case "mousedown": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
|
||||
index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a9c5bb3f8 100644
|
||||
index a76699af75252bec31e17b562463c37236cbbfe5..6355a6ee92754846ffcdd123ac49545d520470c7 100644
|
||||
--- a/browser/components/preferences/preferences.js
|
||||
+++ b/browser/components/preferences/preferences.js
|
||||
@@ -131,6 +131,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
|
||||
@@ -132,6 +132,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
|
||||
styleSheets: [
|
||||
"chrome://browser/skin/preferences/dialog.css",
|
||||
"chrome://browser/skin/preferences/preferences.css",
|
||||
@@ -10,7 +10,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
|
||||
],
|
||||
resizeCallback: async ({ title, frame }) => {
|
||||
// Search within main document and highlight matched keyword.
|
||||
@@ -437,6 +438,8 @@ const CONFIG_PANES = Object.freeze({
|
||||
@@ -462,6 +463,8 @@ const CONFIG_PANES = Object.freeze({
|
||||
tabsBrowsing: {
|
||||
l10nId: "tabs-browsing-section",
|
||||
groupIds: [
|
||||
@@ -19,7 +19,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
|
||||
"browserLayout",
|
||||
"tabs",
|
||||
"pageNavigation",
|
||||
@@ -483,7 +486,7 @@ function register_module(categoryName, categoryObject) {
|
||||
@@ -509,7 +512,7 @@ function register_module(categoryName, categoryObject) {
|
||||
}
|
||||
this._initted = true;
|
||||
let template = document.getElementById("template-" + categoryName);
|
||||
@@ -28,7 +28,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
|
||||
// Replace the template element with the nodes inside of it.
|
||||
template.replaceWith(template.content);
|
||||
|
||||
@@ -527,6 +530,10 @@ function init_all() {
|
||||
@@ -553,6 +556,10 @@ function init_all() {
|
||||
register_module("paneHome", gHomePane);
|
||||
register_module("paneSearch", gSearchPane);
|
||||
register_module("panePrivacy", gPrivacyPane);
|
||||
@@ -39,15 +39,3 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
|
||||
|
||||
// Restore the cached Firefox Labs nav button visibility so it shows
|
||||
// immediately when recipes are expected to be available, before
|
||||
@@ -656,9 +663,9 @@ async function gotoPref(
|
||||
let redesignEnabled = srdSectionPrefs.all;
|
||||
let categories = document.getElementById("categories");
|
||||
const kDefaultCategoryInternalName = redesignEnabled
|
||||
- ? "paneSync"
|
||||
+ ? "paneTabsBrowsing"
|
||||
: "paneGeneral";
|
||||
- const kDefaultCategory = redesignEnabled ? "sync" : "general";
|
||||
+ const kDefaultCategory = redesignEnabled ? "tabsBrowsing" : "general";
|
||||
let hash = document.location.hash;
|
||||
let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ index 1b8c38159314b6edee9e6f455beb2b63db1ad3f0..1308345db640dba6b2848b49ef7c1181
|
||||
+ </html:moz-page-nav-button>
|
||||
+ <html:moz-page-nav-button id="category-zen-CKS"
|
||||
+ view="paneZenCKS"
|
||||
+ iconsrc="chrome://browser/skin/quickactions.svg"
|
||||
+ iconsrc="chrome://browser/skin/preferences/category-accessibility.svg"
|
||||
+ data-l10n-id="pane-zen-CKS-title">
|
||||
+ </html:moz-page-nav-button>
|
||||
+ <html:moz-page-nav-button id="category-zen-marketplace"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
diff --git a/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs b/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
|
||||
index 9359a485ed6212c7df49a8d4b1e4a8febbc1c9b6..132fea440cfa535e78b35f706e7ef2d6881f9ba6 100644
|
||||
--- a/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
|
||||
+++ b/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
|
||||
@@ -50,6 +50,10 @@ const engineTypeToMetadata = {
|
||||
iconSrc: "chrome://global/skin/icons/settings.svg",
|
||||
l10nId: "sync-currently-syncing-settings",
|
||||
},
|
||||
+ workspaces: {
|
||||
+ iconSrc: "chrome://devtools/skin/images/tool-storage.svg",
|
||||
+ l10nId: "sync-currently-syncing-workspaces",
|
||||
+ },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1,18 +1,20 @@
|
||||
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee4aa3be28 100644
|
||||
index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2ab6e6c967 100644
|
||||
--- a/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
@@ -129,6 +129,9 @@ const TAB_EVENTS = [
|
||||
@@ -129,6 +129,11 @@ const TAB_EVENTS = [
|
||||
"TabSplitViewActivate",
|
||||
"SplitViewRemoved",
|
||||
"SplitViewCreated",
|
||||
+ "TabAddedToEssentials",
|
||||
+ "TabRemovedFromEssentials",
|
||||
+ "TabMove",
|
||||
+ "TabGroupMoved",
|
||||
+ "ZenWorkspaceDataChanged",
|
||||
];
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
@@ -201,6 +204,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
@@ -201,6 +206,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
|
||||
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
@@ -21,7 +23,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
|
||||
@@ -1300,10 +1305,7 @@ var SessionStoreInternal = {
|
||||
@@ -1317,10 +1324,7 @@ var SessionStoreInternal = {
|
||||
*/
|
||||
get willAutoRestore() {
|
||||
return (
|
||||
@@ -33,17 +35,19 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1990,6 +1992,9 @@ var SessionStoreInternal = {
|
||||
@@ -2008,6 +2012,11 @@ var SessionStoreInternal = {
|
||||
case "TabPinned":
|
||||
case "TabUnpinned":
|
||||
case "SwapDocShells":
|
||||
+ case "TabRemovedFromEssentials":
|
||||
+ case "TabAddedToEssentials":
|
||||
+ case "TabMove":
|
||||
+ case "TabGroupMoved":
|
||||
+ case "ZenWorkspaceDataChanged":
|
||||
this.saveStateDelayed(win);
|
||||
break;
|
||||
case "TabGroupCreate":
|
||||
@@ -2099,6 +2104,10 @@ var SessionStoreInternal = {
|
||||
@@ -2117,6 +2126,10 @@ var SessionStoreInternal = {
|
||||
this._windows[aWindow.__SSi].isTaskbarTab = true;
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) {
|
||||
this._windows[aWindow.__SSi].isAIWindow = true;
|
||||
}
|
||||
@@ -2135,7 +2144,7 @@ var SessionStoreInternal = {
|
||||
@@ -2153,7 +2166,7 @@ var SessionStoreInternal = {
|
||||
let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
|
||||
// A regular window is not a private window, taskbar tab window, or popup window
|
||||
let isRegularWindow =
|
||||
@@ -63,7 +67,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
// perform additional initialization when the first window is loading
|
||||
if (lazy.RunState.isStopped) {
|
||||
@@ -2147,7 +2156,7 @@ var SessionStoreInternal = {
|
||||
@@ -2165,7 +2178,7 @@ var SessionStoreInternal = {
|
||||
// to disk to NOW() to enforce a full interval before the next write.
|
||||
lazy.SessionSaver.updateLastSaveTime();
|
||||
|
||||
@@ -72,7 +76,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
this._log.debug(
|
||||
"initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later"
|
||||
);
|
||||
@@ -2191,6 +2200,7 @@ var SessionStoreInternal = {
|
||||
@@ -2209,6 +2222,7 @@ var SessionStoreInternal = {
|
||||
null,
|
||||
"sessionstore-one-or-no-tab-restored"
|
||||
);
|
||||
@@ -80,7 +84,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
this._deferredAllWindowsRestored.resolve();
|
||||
}
|
||||
// this window was opened by _openWindowWithState
|
||||
@@ -2240,7 +2250,6 @@ var SessionStoreInternal = {
|
||||
@@ -2258,7 +2272,6 @@ var SessionStoreInternal = {
|
||||
if (closedWindowState) {
|
||||
let newWindowState;
|
||||
if (
|
||||
@@ -88,7 +92,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
!lazy.SessionStartup.willRestore()
|
||||
) {
|
||||
// We want to split the window up into pinned tabs and unpinned tabs.
|
||||
@@ -2276,6 +2285,7 @@ var SessionStoreInternal = {
|
||||
@@ -2294,6 +2307,7 @@ var SessionStoreInternal = {
|
||||
}
|
||||
|
||||
if (newWindowState) {
|
||||
@@ -96,7 +100,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// Ensure that the window state isn't hidden
|
||||
this._restoreCount = 1;
|
||||
let state = { windows: [newWindowState] };
|
||||
@@ -2312,6 +2322,9 @@ var SessionStoreInternal = {
|
||||
@@ -2330,6 +2344,9 @@ var SessionStoreInternal = {
|
||||
});
|
||||
this._shouldRestoreLastSession = false;
|
||||
}
|
||||
@@ -106,7 +110,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
if (this._restoreLastWindow && aWindow.toolbar.visible) {
|
||||
// always reset (if not a popup window)
|
||||
@@ -2462,7 +2475,7 @@ var SessionStoreInternal = {
|
||||
@@ -2480,7 +2497,7 @@ var SessionStoreInternal = {
|
||||
|
||||
var tabbrowser = aWindow.gBrowser;
|
||||
|
||||
@@ -115,7 +119,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
TAB_EVENTS.forEach(function (aEvent) {
|
||||
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
|
||||
@@ -2513,7 +2526,7 @@ var SessionStoreInternal = {
|
||||
@@ -2531,7 +2548,7 @@ var SessionStoreInternal = {
|
||||
|
||||
let isLastRegularWindow =
|
||||
Object.values(this._windows).filter(
|
||||
@@ -124,7 +128,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
).length == 1;
|
||||
this._log.debug(
|
||||
`onClose, closing window isLastRegularWindow? ${isLastRegularWindow}`
|
||||
@@ -2570,8 +2583,8 @@ var SessionStoreInternal = {
|
||||
@@ -2588,8 +2605,8 @@ 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.
|
||||
@@ -135,7 +139,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
}
|
||||
|
||||
completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
|
||||
@@ -2591,8 +2604,9 @@ var SessionStoreInternal = {
|
||||
@@ -2609,8 +2626,9 @@ var SessionStoreInternal = {
|
||||
|
||||
// Save non-private windows if they have at
|
||||
// least one saveable tab or are the last window.
|
||||
@@ -147,7 +151,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
if (!isLastWindow && winData.closedId > -1) {
|
||||
this._addClosedAction(
|
||||
@@ -2668,7 +2682,7 @@ var SessionStoreInternal = {
|
||||
@@ -2686,7 +2704,7 @@ var SessionStoreInternal = {
|
||||
* to call this method again asynchronously (for example, after
|
||||
* a window flush).
|
||||
*/
|
||||
@@ -156,7 +160,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// Make sure SessionStore is still running, and make sure that we
|
||||
// haven't chosen to forget this window.
|
||||
if (
|
||||
@@ -2685,6 +2699,7 @@ var SessionStoreInternal = {
|
||||
@@ -2703,6 +2721,7 @@ var SessionStoreInternal = {
|
||||
// _closedWindows from a previous call to this function.
|
||||
let winIndex = this._closedWindows.indexOf(winData);
|
||||
let alreadyStored = winIndex != -1;
|
||||
@@ -164,7 +168,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
|
||||
let shouldStore = hasSaveableTabs || isLastWindow;
|
||||
|
||||
@@ -3507,7 +3522,7 @@ var SessionStoreInternal = {
|
||||
@@ -3525,7 +3544,7 @@ var SessionStoreInternal = {
|
||||
if (!isPrivateWindow && tabState.isPrivate) {
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +177,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4246,6 +4261,12 @@ var SessionStoreInternal = {
|
||||
@@ -4264,6 +4283,12 @@ var SessionStoreInternal = {
|
||||
Math.min(tabState.index, tabState.entries.length)
|
||||
);
|
||||
tabState.pinned = false;
|
||||
@@ -186,7 +190,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
if (inBackground === false) {
|
||||
aWindow.gBrowser.selectedTab = newTab;
|
||||
@@ -4679,6 +4700,8 @@ var SessionStoreInternal = {
|
||||
@@ -4697,6 +4722,8 @@ var SessionStoreInternal = {
|
||||
// Append the tab if we're opening into a different window,
|
||||
tabIndex: aSource == aTargetWindow ? pos : Infinity,
|
||||
pinned: state.pinned,
|
||||
@@ -195,7 +199,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
userContextId: state.userContextId,
|
||||
skipLoad: true,
|
||||
preferredRemoteType,
|
||||
@@ -5176,9 +5199,10 @@ var SessionStoreInternal = {
|
||||
@@ -5194,9 +5221,10 @@ var SessionStoreInternal = {
|
||||
if (activePageData.title && activePageData.title != activePageData.url) {
|
||||
win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
|
||||
isContentTitle: true,
|
||||
@@ -207,7 +211,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5543,7 +5567,7 @@ var SessionStoreInternal = {
|
||||
@@ -5561,7 +5589,7 @@ var SessionStoreInternal = {
|
||||
|
||||
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
|
||||
let tab = tabbrowser.tabs[i];
|
||||
@@ -216,7 +220,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
removableTabs.push(tab);
|
||||
}
|
||||
}
|
||||
@@ -5656,7 +5680,7 @@ var SessionStoreInternal = {
|
||||
@@ -5674,7 +5702,7 @@ var SessionStoreInternal = {
|
||||
|
||||
// collect the data for all windows
|
||||
for (ix in this._windows) {
|
||||
@@ -225,7 +229,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// window data is still in _statesToRestore
|
||||
continue;
|
||||
}
|
||||
@@ -5800,11 +5824,12 @@ var SessionStoreInternal = {
|
||||
@@ -5818,11 +5846,12 @@ var SessionStoreInternal = {
|
||||
}
|
||||
|
||||
let tabbrowser = aWindow.gBrowser;
|
||||
@@ -239,7 +243,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// update the internal state data for this window
|
||||
for (let tab of tabs) {
|
||||
if (tab == aWindow.FirefoxViewHandler.tab) {
|
||||
@@ -5815,6 +5840,9 @@ var SessionStoreInternal = {
|
||||
@@ -5833,6 +5862,9 @@ var SessionStoreInternal = {
|
||||
tabsData.push(tabData);
|
||||
}
|
||||
|
||||
@@ -249,7 +253,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// update tab group state for this window
|
||||
winData.groups = [];
|
||||
for (let tabGroup of aWindow.gBrowser.tabGroups) {
|
||||
@@ -5831,7 +5859,7 @@ var SessionStoreInternal = {
|
||||
@@ -5849,7 +5881,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) {
|
||||
@@ -258,7 +262,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
winData.title = tabbrowser.tabs[0].label;
|
||||
}
|
||||
winData.selected = selectedIndex;
|
||||
@@ -6033,8 +6061,8 @@ var SessionStoreInternal = {
|
||||
@@ -6051,8 +6083,8 @@ var SessionStoreInternal = {
|
||||
// selectTab represents.
|
||||
let selectTab = 0;
|
||||
if (overwriteTabs) {
|
||||
@@ -269,7 +273,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
selectTab = Math.min(selectTab, winData.tabs.length);
|
||||
}
|
||||
|
||||
@@ -6056,6 +6084,7 @@ var SessionStoreInternal = {
|
||||
@@ -6074,6 +6106,7 @@ var SessionStoreInternal = {
|
||||
if (overwriteTabs) {
|
||||
for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
|
||||
if (!tabbrowser.tabs[i].selected) {
|
||||
@@ -277,7 +281,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
tabbrowser.removeTab(tabbrowser.tabs[i]);
|
||||
}
|
||||
}
|
||||
@@ -6090,6 +6119,12 @@ var SessionStoreInternal = {
|
||||
@@ -6108,6 +6141,12 @@ var SessionStoreInternal = {
|
||||
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
|
||||
);
|
||||
}
|
||||
@@ -290,7 +294,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
// Move the originally open tabs to the end.
|
||||
if (initialTabs) {
|
||||
@@ -6656,6 +6691,25 @@ var SessionStoreInternal = {
|
||||
@@ -6674,6 +6713,25 @@ var SessionStoreInternal = {
|
||||
|
||||
// Most of tabData has been restored, now continue with restoring
|
||||
// attributes that may trigger external events.
|
||||
@@ -316,7 +320,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
if (tabData.pinned) {
|
||||
tabbrowser.pinTab(tab);
|
||||
@@ -6838,6 +6892,9 @@ var SessionStoreInternal = {
|
||||
@@ -6859,6 +6917,9 @@ var SessionStoreInternal = {
|
||||
aWindow.gURLBar.readOnly = false;
|
||||
}
|
||||
}
|
||||
@@ -326,7 +330,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
|
||||
let promiseParts = Promise.withResolvers();
|
||||
aWindow.setTimeout(() => {
|
||||
@@ -7633,7 +7690,7 @@ var SessionStoreInternal = {
|
||||
@@ -7654,7 +7715,7 @@ var SessionStoreInternal = {
|
||||
|
||||
let groupsToSave = new Map();
|
||||
for (let tIndex = 0; tIndex < window.tabs.length; ) {
|
||||
@@ -335,7 +339,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// Adjust window.selected
|
||||
if (tIndex + 1 < window.selected) {
|
||||
window.selected -= 1;
|
||||
@@ -7648,7 +7705,7 @@ var SessionStoreInternal = {
|
||||
@@ -7669,7 +7730,7 @@ var SessionStoreInternal = {
|
||||
);
|
||||
// We don't want to increment tIndex here.
|
||||
continue;
|
||||
@@ -344,7 +348,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
// Convert any open groups into saved groups.
|
||||
let groupStateToSave = window.groups.find(
|
||||
groupState => groupState.id == window.tabs[tIndex].groupId
|
||||
@@ -8193,7 +8250,6 @@ var SessionStoreInternal = {
|
||||
@@ -8233,7 +8294,6 @@ var SessionStoreInternal = {
|
||||
timer.initWithCallback(
|
||||
function () {
|
||||
if (beats <= 0) {
|
||||
@@ -352,7 +356,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
|
||||
Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
|
||||
deferred.resolve();
|
||||
}
|
||||
@@ -8671,6 +8727,7 @@ var SessionStoreInternal = {
|
||||
@@ -8711,6 +8771,7 @@ var SessionStoreInternal = {
|
||||
if (
|
||||
!savedTabGroupState.tabs.length ||
|
||||
this.getSavedTabGroup(savedTabGroupState.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
|
||||
index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b15a030046 100644
|
||||
index 70b37f93c4e8ff4ad9a2452681e468d23914c1b5..dbf2d9c83dafbe0e0b4e6caf3835edc8edf9bcf4 100644
|
||||
--- a/browser/components/tabbrowser/content/drag-and-drop.js
|
||||
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
|
||||
@@ -35,6 +35,9 @@
|
||||
@@ -12,7 +12,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
if (isTab(element) || isSplitViewWrapper(element)) {
|
||||
return element;
|
||||
}
|
||||
@@ -118,6 +121,9 @@
|
||||
@@ -128,6 +131,9 @@
|
||||
}
|
||||
|
||||
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
@@ -22,7 +22,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
if (
|
||||
(dropEffect == "move" || dropEffect == "copy") &&
|
||||
document == draggedTab.ownerDocument &&
|
||||
@@ -138,6 +144,7 @@
|
||||
@@ -148,6 +154,7 @@
|
||||
!draggedTab.group.collapsed
|
||||
) {
|
||||
draggedTab.group.collapsed = true;
|
||||
@@ -30,7 +30,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
}
|
||||
|
||||
if (dropEffect == "move") {
|
||||
@@ -145,10 +152,6 @@
|
||||
@@ -155,10 +162,6 @@
|
||||
|
||||
// Pinned tabs in expanded vertical mode are on a grid format and require
|
||||
// different logic to drag and drop.
|
||||
@@ -41,7 +41,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
this._animateTabMove(event);
|
||||
return;
|
||||
}
|
||||
@@ -287,6 +290,10 @@
|
||||
@@ -297,6 +300,10 @@
|
||||
|
||||
this._tabDropIndicator.hidden = true;
|
||||
event.stopPropagation();
|
||||
@@ -52,7 +52,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
if (draggedTab && dropEffect == "copy") {
|
||||
let duplicatedDraggedTab;
|
||||
let duplicatedTabs = [];
|
||||
@@ -312,12 +319,16 @@
|
||||
@@ -324,12 +331,16 @@
|
||||
let translateOffsetY = oldTranslateY % tabHeight;
|
||||
let newTranslateX = oldTranslateX - translateOffsetX;
|
||||
let newTranslateY = oldTranslateY - translateOffsetY;
|
||||
@@ -73,7 +73,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
|
||||
if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
|
||||
// Update both translate axis for pinned vertical expanded tabs
|
||||
@@ -392,11 +403,13 @@
|
||||
@@ -404,11 +415,13 @@
|
||||
this._dragToPinPromoCard,
|
||||
];
|
||||
let shouldPin =
|
||||
@@ -87,15 +87,15 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
isTab(draggedTab) &&
|
||||
draggedTab.pinned &&
|
||||
this._tabbrowserTabs.arrowScrollbox.contains(event.target);
|
||||
@@ -466,6 +479,7 @@
|
||||
@@ -484,6 +497,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ shouldTranslate = false;
|
||||
if (shouldTranslate) {
|
||||
this.#dropAnimationEndTime = Date.now() + DROP_ANIMATION_GRACE_MS;
|
||||
let translationPromises = [];
|
||||
for (let item of movingTabs) {
|
||||
@@ -477,7 +491,7 @@
|
||||
@@ -496,7 +510,7 @@
|
||||
item.removeAttribute("tabdrop-samewindow");
|
||||
resolve();
|
||||
};
|
||||
@@ -104,7 +104,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
postTransitionCleanup();
|
||||
} else {
|
||||
let onTransitionEnd = transitionendEvent => {
|
||||
@@ -552,7 +566,7 @@
|
||||
@@ -580,7 +594,7 @@
|
||||
if (tab.selected) {
|
||||
selectedTab = tab;
|
||||
indexForSelectedTab = newIndex;
|
||||
@@ -113,7 +113,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
const droppedIntoPinnedArea = dropIndex < gBrowser.pinnedTabCount;
|
||||
const newSplitView = gBrowser.adoptSplitView(tab, {
|
||||
elementIndex: droppedIntoPinnedArea
|
||||
@@ -577,7 +591,7 @@
|
||||
@@ -605,7 +619,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
const newTab = gBrowser.adoptTab(selectedTab, {
|
||||
elementIndex: indexForSelectedTab,
|
||||
selectTab: selectedTab == draggedTab,
|
||||
@@ -611,10 +625,6 @@
|
||||
@@ -639,10 +653,6 @@
|
||||
this._tabbrowserTabs.dragAndDropElements[
|
||||
gBrowser.pinnedTabCount + unpinnedSplitViews.length - 1
|
||||
];
|
||||
@@ -133,7 +133,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -664,6 +674,7 @@
|
||||
@@ -703,6 +713,7 @@
|
||||
|
||||
let nextItem = this._tabbrowserTabs.dragAndDropElements[newIndex];
|
||||
let tabGroup = isTab(nextItem) && nextItem.group;
|
||||
@@ -141,12 +141,15 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
gBrowser.loadTabs(urls, {
|
||||
inBackground,
|
||||
replace,
|
||||
@@ -705,7 +716,16 @@
|
||||
@@ -744,6 +755,22 @@
|
||||
this._expandGroupOnDrop(draggedTab);
|
||||
}
|
||||
this._resetTabsAfterDrop(draggedTab.ownerDocument);
|
||||
-
|
||||
+ if (!dt.mozUserCancelled && dt.dropEffect == "none" && !this._isCustomizing) {
|
||||
this._resetTabsAfterDrop(draggedTab);
|
||||
+ if (
|
||||
+ !dt.mozUserCancelled &&
|
||||
+ dt.dropEffect == "none" &&
|
||||
+ !this._tabbrowserTabs._isCustomizing
|
||||
+ ) {
|
||||
+ const moved = gZenViewSplitter.moveTabToSplitView(event, draggedTab);
|
||||
+ if (moved) {
|
||||
+ delete draggedTab._dragData;
|
||||
@@ -154,12 +157,14 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
+ }
|
||||
+ } else if (dt.mozUserCancelled) {
|
||||
+ gZenViewSplitter.onBrowserDragEndToSplit(event, true);
|
||||
+ if (gZenViewSplitter._lastOpenedTab) gZenViewSplitter._lastOpenedTab._visuallySelected = false;
|
||||
+ if (gZenViewSplitter._lastOpenedTab) {
|
||||
+ gZenViewSplitter._lastOpenedTab._visuallySelected = false;
|
||||
+ }
|
||||
+ }
|
||||
|
||||
if (
|
||||
dt.mozUserCancelled ||
|
||||
dt.dropEffect != "none" ||
|
||||
@@ -927,11 +947,10 @@
|
||||
@@ -1074,11 +1101,10 @@
|
||||
return this.#getHorizontalScrollboxDragTarget(event, ignoreSides);
|
||||
}
|
||||
while (target) {
|
||||
@@ -175,7 +180,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
break;
|
||||
}
|
||||
target = target.parentNode;
|
||||
@@ -961,6 +980,9 @@
|
||||
@@ -1108,6 +1134,9 @@
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -185,17 +190,36 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -1014,7 +1036,8 @@
|
||||
isTabGroupLabel(draggedTab) &&
|
||||
draggedTab._dragData?.expandGroupOnDrop
|
||||
) {
|
||||
- draggedTab.group.collapsed = false;
|
||||
+ draggedTab.group.collapsed = draggedTab.group.hasAttribute("has-active");
|
||||
+ gZenFolders.animateGroupMove(draggedTab.group, true);
|
||||
@@ -1170,14 +1199,20 @@
|
||||
let releaseReservedSpace = () =>
|
||||
this.#releaseSpaceInScrolledContent(periphery);
|
||||
if (group.collapsed) {
|
||||
- // The group's tabs animate back to their full size, so hold on to the
|
||||
- // space reserved for them until they get there.
|
||||
- group.addEventListener(
|
||||
- "TabGroupAnimationComplete",
|
||||
- releaseReservedSpace,
|
||||
- { once: true }
|
||||
- );
|
||||
- group.collapsed = false;
|
||||
+ const expand = !group.hasAttribute("has-active");
|
||||
+ if (expand) {
|
||||
+ // The group's tabs animate back to their full size, so hold on to the
|
||||
+ // space reserved for them until they get there.
|
||||
+ group.addEventListener(
|
||||
+ "TabGroupAnimationComplete",
|
||||
+ releaseReservedSpace,
|
||||
+ { once: true }
|
||||
+ );
|
||||
+ } else {
|
||||
+ releaseReservedSpace();
|
||||
+ }
|
||||
+ group.collapsed = !expand;
|
||||
+ gZenFolders.animateGroupMove(group, true);
|
||||
} else {
|
||||
releaseReservedSpace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,7 +1229,6 @@
|
||||
@@ -1362,7 +1397,6 @@
|
||||
// using updateDragImage. On Linux, we can use a panel.
|
||||
if (platform == "win" || platform == "macosx") {
|
||||
captureListener = function () {
|
||||
@@ -203,7 +227,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
};
|
||||
} else {
|
||||
// Create a panel to use it in setDragImage
|
||||
@@ -1244,7 +1266,6 @@
|
||||
@@ -1400,7 +1434,6 @@
|
||||
);
|
||||
dragImageOffset = dragImageOffset * scale;
|
||||
}
|
||||
@@ -211,7 +235,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
|
||||
// _dragData.offsetX/Y give the coordinates that the mouse should be
|
||||
// positioned relative to the corner of the new window created upon
|
||||
@@ -1263,7 +1284,7 @@
|
||||
@@ -1419,7 +1452,7 @@
|
||||
let dropEffect = this.getDropEffectForTabDrag(event);
|
||||
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
|
||||
let collapseTabGroupDuringDrag =
|
||||
@@ -220,7 +244,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
|
||||
tab._dragData = {
|
||||
offsetX: this._tabbrowserTabs.verticalMode
|
||||
@@ -1273,7 +1294,7 @@
|
||||
@@ -1429,7 +1462,7 @@
|
||||
? event.screenY - window.screenY - tabOffset
|
||||
: event.screenY - window.screenY,
|
||||
scrollPos:
|
||||
@@ -229,7 +253,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
|
||||
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
|
||||
screenX: event.screenX,
|
||||
@@ -1320,6 +1341,7 @@
|
||||
@@ -1522,6 +1555,7 @@
|
||||
if (tabStripItemElement.hasAttribute("dragtarget")) {
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +261,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
let isPinned = tab.pinned;
|
||||
let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
|
||||
let isGrid = this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab);
|
||||
@@ -1686,23 +1708,6 @@
|
||||
@@ -1895,23 +1929,6 @@
|
||||
|
||||
// Slide the relevant tabs to their new position.
|
||||
// non-moving tabs adjust for RTL
|
||||
@@ -261,7 +285,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
// moving tabs don't adjust for RTL
|
||||
for (let item of selectedElements) {
|
||||
if (
|
||||
@@ -1751,7 +1756,6 @@
|
||||
@@ -1960,7 +1977,6 @@
|
||||
for (let item of this._tabbrowserTabs.dragAndDropElements) {
|
||||
delete item._moveTogetherSelectedTabsData;
|
||||
item = elementToMove(item);
|
||||
@@ -269,7 +293,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
item.removeAttribute("multiselected-move-together");
|
||||
}
|
||||
}
|
||||
@@ -2598,7 +2602,6 @@
|
||||
@@ -2812,7 +2828,6 @@
|
||||
tab.style.top = "";
|
||||
tab.style.maxWidth = "";
|
||||
tab.style.pointerEvents = "";
|
||||
@@ -277,7 +301,7 @@ index 8bb1219962a1368f50b53c61041f135c68e35f31..8e72441d26883cbef7a352b00c0fe2b1
|
||||
tab.removeAttribute("small-stack");
|
||||
tab.removeAttribute("big-stack");
|
||||
}
|
||||
@@ -2607,11 +2610,9 @@
|
||||
@@ -2821,11 +2836,9 @@
|
||||
)) {
|
||||
label.style.width = "";
|
||||
label.style.maxWidth = "";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/tabbrowser/content/tab-context-menu.js b/browser/components/tabbrowser/content/tab-context-menu.js
|
||||
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f675a21323 100644
|
||||
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..d7f75471a207cccac6ca3dec49037ac30dd76163 100644
|
||||
--- a/browser/components/tabbrowser/content/tab-context-menu.js
|
||||
+++ b/browser/components/tabbrowser/content/tab-context-menu.js
|
||||
@@ -839,7 +839,8 @@ var TabContextMenu = {
|
||||
@@ -839,13 +839,15 @@ var TabContextMenu = {
|
||||
);
|
||||
contextUnpinSelectedTabs.hidden =
|
||||
!this.contextTab.pinned || !this.multiselected;
|
||||
@@ -12,3 +12,10 @@ index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f6
|
||||
// Build Ask Chat items. The classic layout shows the full ask-chat submenu
|
||||
// (#context_askChat); the alt layout shows only a flat "Summarize Page" item
|
||||
// (#context_askChatSummarize). Hide whichever one this layout doesn't use so
|
||||
// a pref toggle reverses cleanly.
|
||||
if (!this._altTabContextMenu) {
|
||||
document.getElementById("context_askChatSummarize").hidden = true;
|
||||
+ document.getElementById("context_aiSeparator").hidden = true;
|
||||
TabContextMenu.GenAI.buildTabMenu(
|
||||
document.getElementById("context_askChat"),
|
||||
this
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
|
||||
index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da5504f9d8 100644
|
||||
index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db542566eba2 100644
|
||||
--- a/browser/components/tabbrowser/content/tabbrowser.js
|
||||
+++ b/browser/components/tabbrowser/content/tabbrowser.js
|
||||
@@ -513,6 +513,7 @@
|
||||
@@ -514,6 +514,7 @@
|
||||
* @type {MozBrowser[]}
|
||||
*/
|
||||
get splitViewBrowsers() {
|
||||
@@ -10,7 +10,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
const browsers = [];
|
||||
if (this.#activeSplitView) {
|
||||
for (const tab of this.#activeSplitView.tabs) {
|
||||
@@ -611,15 +612,66 @@
|
||||
@@ -612,15 +613,66 @@
|
||||
return this.tabContainer.visibleTabs;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
setSelectedTab(val, metricsContext = null) {
|
||||
if (this.selectedTab === val) {
|
||||
return;
|
||||
@@ -632,6 +684,9 @@
|
||||
@@ -633,6 +685,9 @@
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// Update the tab
|
||||
this.tabbox.selectedTab = val;
|
||||
|
||||
@@ -708,6 +763,10 @@
|
||||
@@ -709,6 +764,10 @@
|
||||
userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (openWindowInfo) {
|
||||
userContextId = openWindowInfo.originAttributes.userContextId;
|
||||
}
|
||||
@@ -809,6 +868,8 @@
|
||||
@@ -810,6 +869,8 @@
|
||||
this.tabpanels.appendChild(panel);
|
||||
|
||||
let tab = this.tabs[0];
|
||||
@@ -109,7 +109,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
tab.linkedPanel = uniqueId;
|
||||
this._selectedTab = tab;
|
||||
this._selectedBrowser = browser;
|
||||
@@ -1178,18 +1239,24 @@
|
||||
@@ -1179,18 +1240,24 @@
|
||||
* The context for the operation for telemetry purposes, defaults to an unknown context.
|
||||
*/
|
||||
pinTab(aTab, { metricsContext = this.TabMetrics.UNKNOWN_CONTEXT } = {}) {
|
||||
@@ -135,7 +135,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
aTab.setAttribute("pinned", "true");
|
||||
this._updateTabBarForPinnedTabs();
|
||||
@@ -1206,16 +1273,25 @@
|
||||
@@ -1207,16 +1274,25 @@
|
||||
* The context for the operation for telemetry purposes, defaults to an unknown context.
|
||||
*/
|
||||
unpinTab(aTab, { metricsContext = this.TabMetrics.UNKNOWN_CONTEXT } = {}) {
|
||||
@@ -162,7 +162,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
});
|
||||
|
||||
aTab.style.marginInlineStart = "";
|
||||
@@ -1430,6 +1506,9 @@
|
||||
@@ -1432,6 +1508,9 @@
|
||||
|
||||
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
|
||||
|
||||
@@ -172,7 +172,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (
|
||||
aIconURL &&
|
||||
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
|
||||
@@ -1439,6 +1518,9 @@
|
||||
@@ -1441,6 +1520,9 @@
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -182,7 +182,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
let browser = this.getBrowserForTab(aTab);
|
||||
browser.mIconURL = aIconURL;
|
||||
@@ -1761,7 +1843,6 @@
|
||||
@@ -1763,7 +1845,6 @@
|
||||
|
||||
// Preview mode should not reset the owner
|
||||
if (!this.#previewMode && !oldTab.selected) {
|
||||
@@ -190,7 +190,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
let lastRelatedTab = this.#lastRelatedTabMap.get(oldTab);
|
||||
@@ -1852,6 +1933,7 @@
|
||||
@@ -1854,6 +1935,7 @@
|
||||
if (!this.#previewMode) {
|
||||
newTab.recordTimeFromUnloadToReload();
|
||||
newTab.updateLastAccessed();
|
||||
@@ -198,7 +198,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
oldTab.updateLastAccessed();
|
||||
// if this is the foreground window, update the last-seen timestamps.
|
||||
if (this.documentGlobal == BrowserWindowTracker.getTopWindow()) {
|
||||
@@ -2066,6 +2148,9 @@
|
||||
@@ -2068,6 +2150,9 @@
|
||||
}
|
||||
|
||||
let activeEl = document.activeElement;
|
||||
@@ -208,7 +208,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// If focus is on the old tab, move it to the new tab.
|
||||
if (activeEl == oldTab) {
|
||||
newTab.focus();
|
||||
@@ -2104,7 +2189,7 @@
|
||||
@@ -2106,7 +2191,7 @@
|
||||
// Focus the location bar if it was previously focused for that tab.
|
||||
// In full screen mode, only bother making the location bar visible
|
||||
// if the tab is a blank one.
|
||||
@@ -217,7 +217,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
let selectURL = () => {
|
||||
if (this._asyncTabSwitching) {
|
||||
// Set _awaitingSetURI flag to suppress popup notification
|
||||
@@ -2392,7 +2477,12 @@
|
||||
@@ -2394,7 +2479,12 @@
|
||||
return this._setTabLabel(aTab, aLabel);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (!aLabel || (isURL && /^about:reader\?url=/.test(aLabel))) {
|
||||
return false;
|
||||
}
|
||||
@@ -2518,7 +2608,7 @@
|
||||
@@ -2520,7 +2610,7 @@
|
||||
newIndex = this.selectedTab._tPos + 1;
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (this.isTabGroupLabel(targetTab)) {
|
||||
throw new Error(
|
||||
"Replacing a tab group label with a tab is not supported"
|
||||
@@ -2793,6 +2883,7 @@
|
||||
@@ -2795,6 +2885,7 @@
|
||||
uriIsAboutBlank,
|
||||
userContextId,
|
||||
skipLoad,
|
||||
@@ -248,7 +248,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} = {}) {
|
||||
let b = document.createXULElement("browser");
|
||||
// Use the JSM global to create the permanentKey, so that if the
|
||||
@@ -2866,8 +2957,7 @@
|
||||
@@ -2868,8 +2959,7 @@
|
||||
// we use a different attribute name for this?
|
||||
b.setAttribute("name", name);
|
||||
}
|
||||
@@ -258,7 +258,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
b.setAttribute("transparent", "true");
|
||||
}
|
||||
|
||||
@@ -3022,7 +3112,7 @@
|
||||
@@ -3024,7 +3114,7 @@
|
||||
|
||||
let panel = this.getPanel(browser);
|
||||
let uniqueId = this._generateUniquePanelID();
|
||||
@@ -267,7 +267,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
aTab.linkedPanel = uniqueId;
|
||||
|
||||
// Inject the <browser> into the DOM if necessary.
|
||||
@@ -3082,8 +3172,8 @@
|
||||
@@ -3084,8 +3174,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) {
|
||||
@@ -278,7 +278,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} else {
|
||||
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
|
||||
}
|
||||
@@ -3268,7 +3358,6 @@
|
||||
@@ -3270,7 +3360,6 @@
|
||||
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
|
||||
tabIndex: tab._tPos + 1,
|
||||
userContextId: tab.userContextId,
|
||||
@@ -286,7 +286,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
focusUrlBar: true,
|
||||
});
|
||||
resolve(this.selectedBrowser);
|
||||
@@ -3378,6 +3467,10 @@
|
||||
@@ -3380,6 +3469,10 @@
|
||||
schemelessInput,
|
||||
hasValidUserGestureActivation = false,
|
||||
textDirectiveUserActivation = false,
|
||||
@@ -297,7 +297,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} = {}
|
||||
) {
|
||||
// all callers of addTab that pass a params object need to pass
|
||||
@@ -3388,10 +3481,25 @@
|
||||
@@ -3390,10 +3483,25 @@
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// If we're opening a foreground tab, set the owner by default.
|
||||
ownerTab ??= inBackground ? null : this.selectedTab;
|
||||
|
||||
@@ -3399,6 +3507,7 @@
|
||||
@@ -3401,6 +3509,7 @@
|
||||
if (this.selectedTab.owner) {
|
||||
this.selectedTab.owner = null;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
// Find the tab that opened this one, if any. This is used for
|
||||
// determining positioning, and inherited attributes such as the
|
||||
@@ -3451,6 +3560,22 @@
|
||||
@@ -3453,6 +3562,22 @@
|
||||
noInitialLabel,
|
||||
skipBackgroundNotify,
|
||||
});
|
||||
@@ -354,7 +354,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (insertTab) {
|
||||
// Insert the tab into the tab container in the correct position.
|
||||
this.#insertTabAtIndex(t, {
|
||||
@@ -3459,6 +3584,7 @@
|
||||
@@ -3461,6 +3586,7 @@
|
||||
ownerTab,
|
||||
openerTab,
|
||||
pinned,
|
||||
@@ -362,7 +362,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
bulkOrderedOpen,
|
||||
tabGroup: tabGroup ?? openerTab?.group,
|
||||
});
|
||||
@@ -3477,6 +3603,7 @@
|
||||
@@ -3479,6 +3605,7 @@
|
||||
openWindowInfo,
|
||||
skipLoad,
|
||||
triggeringRemoteType,
|
||||
@@ -370,7 +370,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}));
|
||||
|
||||
if (focusUrlBar) {
|
||||
@@ -3601,6 +3728,12 @@
|
||||
@@ -3603,6 +3730,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// Additionally send pinned tab events
|
||||
if (pinned) {
|
||||
this.#notifyPinnedStatus(t);
|
||||
@@ -3611,6 +3744,15 @@
|
||||
@@ -3613,6 +3746,15 @@
|
||||
if (!inBackground) {
|
||||
this.selectedTab = t;
|
||||
}
|
||||
@@ -399,7 +399,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -3834,6 +3976,7 @@
|
||||
@@ -3836,6 +3978,7 @@
|
||||
insertBefore = null,
|
||||
isAdoptingGroup = false,
|
||||
metricsContext = this.TabMetrics.UNKNOWN_CONTEXT,
|
||||
@@ -407,7 +407,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} = {}
|
||||
) {
|
||||
if (
|
||||
@@ -3844,9 +3987,6 @@
|
||||
@@ -3846,9 +3989,6 @@
|
||||
!this.isSplitViewWrapper(tabOrSplitView)
|
||||
)
|
||||
) {
|
||||
@@ -417,7 +417,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
@@ -3867,9 +4007,14 @@
|
||||
@@ -3869,9 +4009,14 @@
|
||||
label,
|
||||
isAdoptingGroup
|
||||
);
|
||||
@@ -434,7 +434,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
);
|
||||
group.addTabs(tabsAndSplitViews, metricsContext);
|
||||
|
||||
@@ -3973,7 +4118,7 @@
|
||||
@@ -3975,7 +4120,7 @@
|
||||
}
|
||||
|
||||
this.#handleTabMove(tab, () =>
|
||||
@@ -443,7 +443,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4059,6 +4204,7 @@
|
||||
@@ -4061,6 +4206,7 @@
|
||||
color: group.color,
|
||||
insertBefore: newTabs[0],
|
||||
isAdoptingGroup: true,
|
||||
@@ -451,7 +451,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4269,6 +4415,7 @@
|
||||
@@ -4271,6 +4417,7 @@
|
||||
openWindowInfo,
|
||||
skipLoad,
|
||||
triggeringRemoteType,
|
||||
@@ -459,7 +459,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
) {
|
||||
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
|
||||
@@ -4329,6 +4476,7 @@
|
||||
@@ -4331,6 +4478,7 @@
|
||||
openWindowInfo,
|
||||
name,
|
||||
skipLoad,
|
||||
@@ -467,7 +467,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4542,9 +4690,9 @@
|
||||
@@ -4544,9 +4692,9 @@
|
||||
}
|
||||
|
||||
// Add a new tab if needed.
|
||||
@@ -479,7 +479,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
let url = "about:blank";
|
||||
if (tabData.entries?.length) {
|
||||
@@ -4577,8 +4725,10 @@
|
||||
@@ -4579,8 +4727,10 @@
|
||||
insertTab: false,
|
||||
skipLoad: true,
|
||||
preferredRemoteType,
|
||||
@@ -491,7 +491,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (select) {
|
||||
tabToSelect = tab;
|
||||
}
|
||||
@@ -4600,7 +4750,8 @@
|
||||
@@ -4602,7 +4752,8 @@
|
||||
this.pinTab(tab);
|
||||
// Then ensure all the tab open/pinning information is sent.
|
||||
this._fireTabOpen(tab, {});
|
||||
@@ -501,7 +501,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
let { groupId } = tabData;
|
||||
const tabGroup = tabGroupWorkingData.get(groupId);
|
||||
// if a tab refers to a tab group we don't know, skip any group
|
||||
@@ -4620,7 +4771,10 @@
|
||||
@@ -4622,7 +4773,10 @@
|
||||
tabGroup.stateData.id,
|
||||
tabGroup.stateData.color,
|
||||
tabGroup.stateData.collapsed,
|
||||
@@ -513,7 +513,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
);
|
||||
tabsFragment.appendChild(tabGroup.node);
|
||||
}
|
||||
@@ -4675,9 +4829,21 @@
|
||||
@@ -4677,9 +4831,21 @@
|
||||
// to remove the old selected tab.
|
||||
if (tabToSelect) {
|
||||
let leftoverTab = this.selectedTab;
|
||||
@@ -535,7 +535,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
if (tabs.length > 1 || !tabs[0].selected) {
|
||||
this._updateTabsAfterInsert();
|
||||
@@ -4882,11 +5048,17 @@
|
||||
@@ -4884,11 +5050,17 @@
|
||||
if (ownerTab) {
|
||||
tab.owner = ownerTab;
|
||||
}
|
||||
@@ -554,7 +554,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (
|
||||
!bulkOrderedOpen &&
|
||||
((openerTab &&
|
||||
@@ -4898,7 +5070,7 @@
|
||||
@@ -4900,7 +5072,7 @@
|
||||
let lastRelatedTab =
|
||||
openerTab && this.#lastRelatedTabMap.get(openerTab);
|
||||
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
|
||||
@@ -563,7 +563,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
tabGroup = previousTab.group;
|
||||
}
|
||||
if (
|
||||
@@ -4914,7 +5086,7 @@
|
||||
@@ -4916,7 +5088,7 @@
|
||||
previousTab.splitview
|
||||
) + 1;
|
||||
} else if (previousTab.visible) {
|
||||
@@ -572,7 +572,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} else if (previousTab == FirefoxViewHandler.tab) {
|
||||
elementIndex = 0;
|
||||
}
|
||||
@@ -4942,14 +5114,14 @@
|
||||
@@ -4944,14 +5116,14 @@
|
||||
}
|
||||
// Ensure index is within bounds.
|
||||
if (tab.pinned) {
|
||||
@@ -591,7 +591,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
if (pinned && !itemAfter?.pinned) {
|
||||
itemAfter = null;
|
||||
@@ -4966,7 +5138,7 @@
|
||||
@@ -4968,7 +5140,7 @@
|
||||
|
||||
this.tabContainer._invalidateCachedTabs();
|
||||
|
||||
@@ -600,7 +600,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (
|
||||
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
|
||||
this.isSplitViewWrapper(itemAfter)
|
||||
@@ -4997,7 +5169,11 @@
|
||||
@@ -4999,7 +5171,11 @@
|
||||
const tabContainer = pinned
|
||||
? this.tabContainer.pinnedTabsContainer
|
||||
: this.tabContainer;
|
||||
@@ -612,7 +612,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
if (tab.group?.collapsed) {
|
||||
@@ -5012,6 +5188,7 @@
|
||||
@@ -5014,6 +5190,7 @@
|
||||
if (pinned) {
|
||||
this._updateTabBarForPinnedTabs();
|
||||
}
|
||||
@@ -620,7 +620,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
TabBarVisibility.update();
|
||||
}
|
||||
@@ -5568,6 +5745,7 @@
|
||||
@@ -5577,6 +5754,7 @@
|
||||
metricsContext,
|
||||
} = {}
|
||||
) {
|
||||
@@ -628,7 +628,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
|
||||
// can be considered equivalent to closing the window.
|
||||
if (
|
||||
@@ -5678,6 +5856,7 @@
|
||||
@@ -5687,6 +5865,7 @@
|
||||
closedTabCount -= 1;
|
||||
}
|
||||
}
|
||||
@@ -636,7 +636,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
if (closedTabCount > 0) {
|
||||
this.recordTabMetrics(
|
||||
@@ -5730,6 +5909,14 @@
|
||||
@@ -5761,6 +5940,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -651,7 +651,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
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
|
||||
@@ -5737,6 +5924,9 @@
|
||||
@@ -5768,6 +5955,9 @@
|
||||
// state).
|
||||
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
|
||||
let isLastTab = this.#isLastTabInWindow(aTab);
|
||||
@@ -661,7 +661,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (
|
||||
!this._beginRemoveTab(aTab, {
|
||||
closeWindowFastpath: true,
|
||||
@@ -5747,13 +5937,14 @@
|
||||
@@ -5778,13 +5968,14 @@
|
||||
metricsContext,
|
||||
})
|
||||
) {
|
||||
@@ -677,7 +677,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
let lockTabSizing =
|
||||
!this.tabContainer.verticalMode &&
|
||||
!aTab.pinned &&
|
||||
@@ -5784,7 +5975,13 @@
|
||||
@@ -5815,7 +6006,13 @@
|
||||
// We're not animating, so we can cancel the animation stopwatch.
|
||||
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
|
||||
aTab._closeTimeAnimTimerId = null;
|
||||
@@ -692,7 +692,18 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5827,7 +6024,7 @@
|
||||
@@ -5855,7 +6052,9 @@
|
||||
get #shouldCloseWindowWithLastTab() {
|
||||
return (
|
||||
!window.toolbar.visible ||
|
||||
- Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
|
||||
+ (Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab") &&
|
||||
+ !gZenWorkspaces._isClosingWindow &&
|
||||
+ !gZenWorkspaces._removedByStartupPage)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5871,7 +6070,7 @@
|
||||
*/
|
||||
#isLastTabInWindow(tab) {
|
||||
for (const otherTab of this.tabs) {
|
||||
@@ -701,16 +712,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5917,7 +6114,7 @@
|
||||
closeWindowWithLastTab != null
|
||||
? closeWindowWithLastTab
|
||||
: !window.toolbar.visible ||
|
||||
- Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
|
||||
+ Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab") && !gZenWorkspaces._isClosingWindow && !gZenWorkspaces._removedByStartupPage;
|
||||
|
||||
if (closeWindow) {
|
||||
// We've already called beforeunload on all the relevant tabs if we get here,
|
||||
@@ -5941,6 +6138,7 @@
|
||||
@@ -5982,6 +6181,7 @@
|
||||
|
||||
newTab = true;
|
||||
}
|
||||
@@ -718,8 +720,8 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
aTab._endRemoveArgs = [closeWindow, newTab];
|
||||
|
||||
// swapBrowsersAndCloseOther will take care of closing the window without animation.
|
||||
@@ -5982,13 +6180,7 @@
|
||||
aTab._mouseleave();
|
||||
@@ -6029,13 +6229,7 @@
|
||||
}
|
||||
|
||||
if (newTab) {
|
||||
- this.addTrustedTab(BROWSER_NEW_TAB_URL, {
|
||||
@@ -733,7 +735,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} else {
|
||||
TabBarVisibility.update();
|
||||
}
|
||||
@@ -6128,6 +6320,7 @@
|
||||
@@ -6192,6 +6386,7 @@
|
||||
this.tabs[i]._tPos = i;
|
||||
}
|
||||
|
||||
@@ -741,7 +743,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (!this.#windowIsClosing) {
|
||||
// update tab close buttons state
|
||||
this.tabContainer._updateCloseButtons();
|
||||
@@ -6313,6 +6506,7 @@
|
||||
@@ -6377,6 +6572,7 @@
|
||||
memory_after: await getTotalMemoryUsage(),
|
||||
time_to_unload_in_ms: timeElapsed,
|
||||
});
|
||||
@@ -749,7 +751,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6358,6 +6552,7 @@
|
||||
@@ -6422,6 +6618,7 @@
|
||||
}
|
||||
|
||||
let excludeTabs = new Set(aExcludeTabs);
|
||||
@@ -757,7 +759,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
// If this tab has a successor, it should be selectable, since
|
||||
// hiding or closing a tab removes that tab as a successor.
|
||||
@@ -6370,13 +6565,13 @@
|
||||
@@ -6434,13 +6631,13 @@
|
||||
!excludeTabs.has(aTab.owner) &&
|
||||
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
|
||||
) {
|
||||
@@ -773,7 +775,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
);
|
||||
|
||||
if (Services.prefs.getBoolPref("browser.tabs.selectMRUOnClose", false)) {
|
||||
@@ -6392,6 +6587,13 @@
|
||||
@@ -6456,6 +6653,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +789,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
let tab = this.tabContainer.findNextTab(aTab, {
|
||||
direction: 1,
|
||||
filter: _tab => remainingTabs.includes(_tab),
|
||||
@@ -6405,7 +6607,7 @@
|
||||
@@ -6469,7 +6673,7 @@
|
||||
}
|
||||
|
||||
if (tab) {
|
||||
@@ -796,7 +798,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
// If no qualifying visible tab was found, see if there is a tab in
|
||||
@@ -6426,7 +6628,7 @@
|
||||
@@ -6490,7 +6694,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
@@ -805,7 +807,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
_blurTab(aTab) {
|
||||
@@ -6437,7 +6639,7 @@
|
||||
@@ -6501,7 +6705,7 @@
|
||||
* @returns {boolean}
|
||||
* False if swapping isn't permitted, true otherwise.
|
||||
*/
|
||||
@@ -814,7 +816,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// Do not allow transfering a private tab to a non-private window
|
||||
// and vice versa.
|
||||
if (
|
||||
@@ -6491,6 +6693,7 @@
|
||||
@@ -6555,6 +6759,7 @@
|
||||
// fire the beforeunload event in the process. Close the other
|
||||
// window if this was its last tab.
|
||||
if (
|
||||
@@ -822,7 +824,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
!remoteBrowser._beginRemoveTab(aOtherTab, {
|
||||
adoptedByTab: aOurTab,
|
||||
closeWindowWithLastTab: true,
|
||||
@@ -6502,7 +6705,7 @@
|
||||
@@ -6566,7 +6771,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.
|
||||
@@ -831,7 +833,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (closeWindow) {
|
||||
let win = aOtherTab.documentGlobal;
|
||||
win.windowUtils.suppressAnimation(true);
|
||||
@@ -6636,11 +6839,13 @@
|
||||
@@ -6700,11 +6905,13 @@
|
||||
}
|
||||
|
||||
// Finish tearing down the tab that's going away.
|
||||
@@ -845,7 +847,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
this.setTabTitle(aOurTab);
|
||||
|
||||
@@ -6860,10 +7065,10 @@
|
||||
@@ -6928,10 +7135,10 @@
|
||||
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
|
||||
}
|
||||
|
||||
@@ -858,7 +860,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
aTab.selected ||
|
||||
aTab.closing ||
|
||||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
|
||||
@@ -6923,7 +7128,8 @@
|
||||
@@ -6991,7 +7198,8 @@
|
||||
* @param {object} [aOptions={}]
|
||||
* Key-value pairs that will be serialized into the features string.
|
||||
*/
|
||||
@@ -868,7 +870,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
if (this.tabs.length == 1) {
|
||||
return null;
|
||||
}
|
||||
@@ -6940,7 +7146,7 @@
|
||||
@@ -7008,7 +7216,7 @@
|
||||
// tell a new window to take the "dropped" tab
|
||||
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
||||
args.appendElement(aTab.splitview ?? aTab);
|
||||
@@ -877,7 +879,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
private: PrivateBrowsingUtils.isWindowPrivate(window),
|
||||
features: Object.entries(aOptions)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
@@ -6948,6 +7154,8 @@
|
||||
@@ -7016,6 +7224,8 @@
|
||||
openerWindow: window,
|
||||
args,
|
||||
});
|
||||
@@ -886,7 +888,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7074,7 +7282,7 @@
|
||||
@@ -7142,7 +7352,7 @@
|
||||
* `true` if element is a `<tab-group>`
|
||||
*/
|
||||
isTabGroup(element) {
|
||||
@@ -895,7 +897,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7146,8 +7354,8 @@
|
||||
@@ -7214,8 +7424,8 @@
|
||||
}
|
||||
|
||||
// Don't allow mixing pinned and unpinned tabs.
|
||||
@@ -906,7 +908,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} else {
|
||||
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
|
||||
}
|
||||
@@ -7193,8 +7401,8 @@
|
||||
@@ -7261,8 +7471,8 @@
|
||||
this.#handleTabMove(
|
||||
element,
|
||||
() => {
|
||||
@@ -917,7 +919,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
neighbor = neighbor.group;
|
||||
}
|
||||
if (neighbor?.splitview) {
|
||||
@@ -7205,6 +7413,12 @@
|
||||
@@ -7273,6 +7483,12 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -930,7 +932,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
if (movingForwards && neighbor) {
|
||||
neighbor.after(element);
|
||||
@@ -7278,23 +7492,31 @@
|
||||
@@ -7346,23 +7562,31 @@
|
||||
) {
|
||||
if (this.isTabGroupLabel(targetElement)) {
|
||||
targetElement = targetElement.group;
|
||||
@@ -968,7 +970,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
} 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
|
||||
@@ -7307,12 +7529,35 @@
|
||||
@@ -7375,12 +7599,35 @@
|
||||
// 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.
|
||||
@@ -1005,7 +1007,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
// 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.
|
||||
@@ -7321,6 +7566,7 @@
|
||||
@@ -7389,6 +7636,7 @@
|
||||
}
|
||||
|
||||
let getContainer = () =>
|
||||
@@ -1013,7 +1015,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
element.pinned
|
||||
? this.tabContainer.pinnedTabsContainer
|
||||
: this.tabContainer;
|
||||
@@ -7329,11 +7575,15 @@
|
||||
@@ -7397,11 +7645,15 @@
|
||||
element,
|
||||
() => {
|
||||
if (moveBefore) {
|
||||
@@ -1030,7 +1032,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
},
|
||||
{ metricsContext }
|
||||
@@ -7411,11 +7661,15 @@
|
||||
@@ -7479,11 +7731,15 @@
|
||||
* The context for the operation for telemetry purposes.
|
||||
*/
|
||||
moveTabToExistingGroup(aTab, aGroup, { metricsContext } = {}) {
|
||||
@@ -1049,7 +1051,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
}
|
||||
if (aTab.group && aTab.group.id === aGroup.id) {
|
||||
return;
|
||||
@@ -7489,6 +7743,7 @@
|
||||
@@ -7557,6 +7813,7 @@
|
||||
|
||||
let state = {
|
||||
tabIndex: tab._tPos,
|
||||
@@ -1057,7 +1059,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
};
|
||||
if (tab.visible) {
|
||||
state.elementIndex = tab.elementIndex;
|
||||
@@ -7527,7 +7782,7 @@
|
||||
@@ -7595,7 +7852,7 @@
|
||||
let changedSplitView =
|
||||
previousTabState.splitViewId != currentTabState.splitViewId;
|
||||
|
||||
@@ -1066,7 +1068,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
tab.dispatchEvent(
|
||||
new CustomEvent("TabMove", {
|
||||
bubbles: true,
|
||||
@@ -7579,6 +7834,10 @@
|
||||
@@ -7647,6 +7904,10 @@
|
||||
|
||||
moveActionCallback();
|
||||
|
||||
@@ -1077,7 +1079,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// Clear tabs cache after moving nodes because the order of tabs may have
|
||||
// changed.
|
||||
this.tabContainer._invalidateCachedTabs();
|
||||
@@ -7626,7 +7885,22 @@
|
||||
@@ -7694,7 +7955,22 @@
|
||||
* @returns {object}
|
||||
* The new tab in the current window, null if the tab couldn't be adopted.
|
||||
*/
|
||||
@@ -1101,7 +1103,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
// 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
|
||||
@@ -7669,6 +7943,8 @@
|
||||
@@ -7737,6 +8013,8 @@
|
||||
}
|
||||
params.skipLoad = true;
|
||||
let newTab = this.addWebTab("about:blank", params);
|
||||
@@ -1110,7 +1112,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
aTab.container.tabDragAndDrop.finishAnimateTabMove();
|
||||
|
||||
@@ -8448,7 +8724,7 @@
|
||||
@@ -8516,7 +8794,7 @@
|
||||
// preventDefault(). It will still raise the window if appropriate.
|
||||
return;
|
||||
}
|
||||
@@ -1119,7 +1121,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
window.focus();
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
@@ -8465,7 +8741,6 @@
|
||||
@@ -8533,7 +8811,6 @@
|
||||
|
||||
on_TabGroupCollapse(aEvent) {
|
||||
aEvent.target.tabs.forEach(tab => {
|
||||
@@ -1127,7 +1129,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8819,7 +9094,9 @@
|
||||
@@ -8887,7 +9164,9 @@
|
||||
|
||||
let filter = this.#tabFilters.get(tab);
|
||||
if (filter) {
|
||||
@@ -1137,7 +1139,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
|
||||
let listener = this.#tabListeners.get(tab);
|
||||
if (listener) {
|
||||
@@ -9604,6 +9881,7 @@
|
||||
@@ -9676,6 +9955,7 @@
|
||||
aWebProgress.isTopLevel
|
||||
) {
|
||||
this._tab.setAttribute("busy", "true");
|
||||
@@ -1145,7 +1147,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
gBrowser._tabAttrModified(this._tab, ["busy"]);
|
||||
this._tab._notselectedsinceload = !this._tab.selected;
|
||||
}
|
||||
@@ -9684,6 +9962,7 @@
|
||||
@@ -9756,6 +10036,7 @@
|
||||
// known defaults. Note we use the original URL since about:newtab
|
||||
// redirects to a prerendered page.
|
||||
const shouldRemoveFavicon =
|
||||
@@ -1153,7 +1155,7 @@ index 39b5babee5942427a82a7a64ad69e1b16efebcc1..32beac13dd4215a16de3a783db6c32da
|
||||
!this._browser.mIconURL &&
|
||||
!ignoreBlank &&
|
||||
!(originalLocation.spec in FAVICON_DEFAULTS);
|
||||
@@ -9858,13 +10137,6 @@
|
||||
@@ -9930,13 +10211,6 @@
|
||||
this._browser.originalURI = aRequest.originalURI;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644
|
||||
index e48f0295d803ba9eac5ab9d01ebb3b3dacbc9d4b..cb78b74230f63a47479404767b5ab5883278dfba 100644
|
||||
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
||||
@@ -161,6 +161,9 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
||||
// Feature gate pref for flight status suggestions in the urlbar.
|
||||
["flightStatus.featureGate", false],
|
||||
|
||||
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
|
||||
+ ["closeOnWindowBlur", true],
|
||||
+
|
||||
// The minimum prefix length of a flight status keyword the user must type to
|
||||
// trigger the suggestion. 0 means the min length should be taken from Nimbus
|
||||
// or remote settings.
|
||||
@@ -486,6 +489,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
||||
["shortcuts.tabs", true],
|
||||
["shortcuts.history", true],
|
||||
["shortcuts.actions", true],
|
||||
@@ -10,12 +20,12 @@ index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489f
|
||||
|
||||
// Boolean to determine if the providers defined in `exposureResults`
|
||||
// should be displayed in search results. This can be set by a
|
||||
@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
|
||||
@@ -854,6 +858,8 @@ function makeDefaultResultGroups({
|
||||
*/
|
||||
let rootGroup = {
|
||||
children: [
|
||||
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }] },
|
||||
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_WORKSPACE }] },
|
||||
+ { children: [{ group: lazy.UrlbarShared.RESULT_GROUP.ZEN_ACTION }] },
|
||||
+ { children: [{ group: lazy.UrlbarShared.RESULT_GROUP.ZEN_WORKSPACE }] },
|
||||
// heuristic
|
||||
{
|
||||
maxResultCount: 1,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs b/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
|
||||
index f611d8e44af518aa1adc7505c5021235f55b49de..0eca20cb869626dfa6ace02acc00cad84462d54f 100644
|
||||
index cfc232f038cd491db32e0c0ca35622360dd9dec5..4806a725ddefa9d0709020d537eae5464249a294 100644
|
||||
--- a/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
|
||||
+++ b/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
|
||||
@@ -78,22 +78,26 @@ export class UrlbarProviderHeuristicFallback extends UrlbarProvider {
|
||||
@@ -77,22 +77,26 @@ export class UrlbarProviderHeuristicFallback extends UrlbarProvider {
|
||||
// Since we can't tell if this is a real URL and whether the user wants
|
||||
// to visit or search for it, we provide an alternative searchengine
|
||||
// match if the string looks like an alphanumeric origin or an e-mail.
|
||||
@@ -17,7 +17,7 @@ index f611d8e44af518aa1adc7505c5021235f55b49de..0eca20cb869626dfa6ace02acc00cad8
|
||||
- lazy.UrlUtils.REGEXP_COMMON_EMAIL.test(str))
|
||||
- ) {
|
||||
+ let trimmedSearchString = queryContext.trimmedSearchString;
|
||||
+ let [searchCandidate] = UrlbarUtils.stripPrefixAndTrim(
|
||||
+ let [searchCandidate] = lazy.UrlbarShared.stripPrefixAndTrim(
|
||||
+ trimmedSearchString,
|
||||
+ {
|
||||
+ trimSlash: true,
|
||||
|
||||
@@ -1,41 +1,16 @@
|
||||
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
|
||||
index 78c76bde00b6b581184cee1cd0e1756e5f2da6a4..94c713470b1240729ef2350d090b0449f3801af4 100644
|
||||
index 178cd5e84fda74a28b2de59b671b3399cc28fac4..b03c6e06de4ee9a95dcb4d33d7ac9f0b053be4e1 100644
|
||||
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
|
||||
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
|
||||
@@ -126,6 +126,8 @@ export var UrlbarUtils = {
|
||||
SEMANTIC_HISTORY: "semanticHistory",
|
||||
SUGGESTED_INDEX: "suggestedIndex",
|
||||
TAIL_SUGGESTION: "tailSuggestion",
|
||||
+ ZEN_ACTION: "zenAction",
|
||||
+ ZEN_WORKSPACE: "zenWorkspace",
|
||||
}),
|
||||
|
||||
// Defines provider types.
|
||||
@@ -274,6 +276,14 @@ export var UrlbarUtils = {
|
||||
telemetryLabel: "actions",
|
||||
uiLabel: "urlbar-searchmode-actions3",
|
||||
},
|
||||
+ {
|
||||
+ source: UrlbarShared.RESULT_SOURCE.WORKSPACES,
|
||||
+ restrict: UrlbarShared.RESTRICT_TOKENS.WORKSPACE,
|
||||
+ icon: "chrome://browser/skin/zen-icons/selectable/layers.svg",
|
||||
+ pref: "shortcuts.workspaces",
|
||||
+ telemetryLabel: "workspaces",
|
||||
+ uiLabel: "urlbar-search-mode-workspaces",
|
||||
+ },
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -566,6 +576,12 @@ export var UrlbarUtils = {
|
||||
return this.RESULT_GROUP.HEURISTIC_FALLBACK;
|
||||
@@ -214,6 +214,11 @@ export var UrlbarUtils = {
|
||||
return UrlbarShared.RESULT_GROUP.HEURISTIC_FALLBACK;
|
||||
case "UrlbarProviderHistoryUrlHeuristic":
|
||||
return this.RESULT_GROUP.HEURISTIC_HISTORY_URL;
|
||||
return UrlbarShared.RESULT_GROUP.HEURISTIC_HISTORY_URL;
|
||||
+ case "ZenUrlbarProviderGlobalActions":
|
||||
+ if (result.source == UrlbarShared.RESULT_SOURCE.WORKSPACES) {
|
||||
+ return this.RESULT_GROUP.ZEN_WORKSPACE;
|
||||
+ } else {
|
||||
+ return this.RESULT_GROUP.ZEN_ACTION;
|
||||
+ return UrlbarShared.RESULT_GROUP.ZEN_WORKSPACE;
|
||||
+ }
|
||||
+ return UrlbarShared.RESULT_GROUP.ZEN_ACTION;
|
||||
case "UrlbarProviderOmnibox":
|
||||
return this.RESULT_GROUP.HEURISTIC_OMNIBOX;
|
||||
return UrlbarShared.RESULT_GROUP.HEURISTIC_OMNIBOX;
|
||||
case "UrlbarProviderRestrictKeywordsAutofill":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarChildController.mjs b/browser/components/urlbar/content/UrlbarChildController.mjs
|
||||
index a8e9d3093d046612650a17970330a8279f55833f..063a3e8adf009d6c0ddc60430bc0e7485e812b8a 100644
|
||||
index a17a4de079b69d2818388b33a2c1746cc6148c73..4fee63a22919972af08716db2d2de1c7549ff100 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarChildController.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarChildController.mjs
|
||||
@@ -258,7 +258,6 @@ export class UrlbarChildController {
|
||||
@@ -438,7 +438,6 @@ export class UrlbarChildController {
|
||||
const isMac = AppConstants.platform == "macosx";
|
||||
// Handle readline/emacs-style navigation bindings on Mac.
|
||||
if (
|
||||
@@ -10,7 +10,7 @@ index a8e9d3093d046612650a17970330a8279f55833f..063a3e8adf009d6c0ddc60430bc0e748
|
||||
this.view.isOpen &&
|
||||
event.ctrlKey &&
|
||||
(event.key == "n" || event.key == "p")
|
||||
@@ -450,6 +449,8 @@ export class UrlbarChildController {
|
||||
@@ -630,6 +629,8 @@ export class UrlbarChildController {
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -19,3 +19,25 @@ index a8e9d3093d046612650a17970330a8279f55833f..063a3e8adf009d6c0ddc60430bc0e748
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -934,6 +935,10 @@ export class UrlbarChildController {
|
||||
}
|
||||
reuseEmpty = true;
|
||||
}
|
||||
+ if (this.#input.hasAttribute?.("zen-newtab")) {
|
||||
+ where = "tab";
|
||||
+ reuseEmpty = true;
|
||||
+ }
|
||||
// The browser window exists only in chrome; a content-process input has no
|
||||
// tab to reuse, so `gBrowser` is absent and the reuse is skipped.
|
||||
if (
|
||||
@@ -943,6 +948,10 @@ export class UrlbarChildController {
|
||||
) {
|
||||
where = "current";
|
||||
}
|
||||
+ if (this.window.gBrowser?.selectedTab.hasAttribute("zen-empty-tab")) {
|
||||
+ // Always open in a new tab if the current tab is our empty tab.
|
||||
+ return "tab";
|
||||
+ }
|
||||
return where;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a6182579cbaf4d 100644
|
||||
index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1a95e28b8 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
@@ -101,6 +101,13 @@ const lazy = XPCOMUtils.declareLazy({
|
||||
@@ -88,6 +88,13 @@ const lazy = XPCOMUtils.declareLazy({
|
||||
logger: () => UrlbarShared.getLogger({ prefix: "Input" }),
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
const UNLIMITED_MAX_RESULTS = 99;
|
||||
|
||||
let getBoundsWithoutFlushing = element =>
|
||||
@@ -769,7 +776,16 @@ ${
|
||||
@@ -771,7 +778,16 @@ ${
|
||||
// See _on_select(). HTMLInputElement.select() dispatches a "select"
|
||||
// event but does not set the primary selection.
|
||||
this._suppressPrimaryAdjustment = true;
|
||||
@@ -33,7 +33,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
this._suppressPrimaryAdjustment = false;
|
||||
}
|
||||
|
||||
@@ -843,6 +859,10 @@ ${
|
||||
@@ -846,6 +862,10 @@ ${
|
||||
hideSearchTerms = false,
|
||||
isSameDocument = false,
|
||||
} = {}) {
|
||||
@@ -44,7 +44,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
if (!this.#isAddressbar) {
|
||||
throw new Error(
|
||||
"Cannot set URI for UrlbarInput that is not an address bar"
|
||||
@@ -1137,7 +1157,16 @@ ${
|
||||
@@ -1143,7 +1163,16 @@ ${
|
||||
this.searchModeSwitcher?.updateSearchIcon();
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1626,7 +1655,11 @@ ${
|
||||
@@ -1726,7 +1755,11 @@ ${
|
||||
openParams.avoidBrowserFocus = keepViewOpen;
|
||||
|
||||
if (!this.#providesSearchMode(result) && !keepViewOpen) {
|
||||
@@ -74,7 +74,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
}
|
||||
|
||||
if (isCanonized) {
|
||||
@@ -2974,6 +3007,42 @@ ${
|
||||
@@ -3016,6 +3049,42 @@ ${
|
||||
await this.#updateLayoutBreakoutDimensions();
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
startLayoutExtend() {
|
||||
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
|
||||
// Do not expand if the Urlbar does not support being expanded or it is
|
||||
@@ -2988,6 +3057,13 @@ ${
|
||||
@@ -3030,6 +3099,13 @@ ${
|
||||
this.toggleAttribute("breakout-extend", true);
|
||||
this.#updateTextboxPosition();
|
||||
|
||||
@@ -131,7 +131,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
// Enable the animation only after the first extend call to ensure it
|
||||
// doesn't run when opening a new window.
|
||||
if (!this.hasAttribute("breakout-extend-animate")) {
|
||||
@@ -3011,6 +3087,29 @@ ${
|
||||
@@ -3053,6 +3129,29 @@ ${
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
this.toggleAttribute("breakout-extend", false);
|
||||
this.#updateTextboxPosition();
|
||||
}
|
||||
@@ -3049,7 +3148,7 @@ ${
|
||||
@@ -3091,7 +3190,7 @@ ${
|
||||
forceUnifiedSearchButtonAvailable = false
|
||||
) {
|
||||
let prevState = this.getAttribute("pageproxystate");
|
||||
@@ -170,7 +170,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
this.setAttribute("pageproxystate", state);
|
||||
this._inputContainer.setAttribute("pageproxystate", state);
|
||||
this._identityBox?.setAttribute("pageproxystate", state);
|
||||
@@ -3332,10 +3431,12 @@ ${
|
||||
@@ -3355,10 +3454,12 @@ ${
|
||||
}
|
||||
|
||||
this.style.top = px(
|
||||
@@ -183,7 +183,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3394,9 +3495,10 @@ ${
|
||||
@@ -3417,9 +3518,10 @@ ${
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,9 +193,9 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
- px(getBoundsWithoutFlushing(this.parentNode).height)
|
||||
+ px(getBoundsWithoutFlushing(this.parentNode).height + 8)
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--urlbar-height",
|
||||
@@ -3902,6 +4004,7 @@ ${
|
||||
|
||||
if (this.#breakoutBlockerCount) {
|
||||
@@ -3929,6 +4031,7 @@ ${
|
||||
}
|
||||
|
||||
_toggleActionOverride(event) {
|
||||
@@ -203,47 +203,33 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
if (
|
||||
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
||||
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
||||
@@ -4014,8 +4117,8 @@ ${
|
||||
@@ -4027,9 +4130,10 @@ ${
|
||||
if (!this.#isAddressbar) {
|
||||
return val;
|
||||
}
|
||||
- let trimmedValue = lazy.UrlbarPrefs.get("trimURLs")
|
||||
- let trimmedValue = UrlbarPrefs.get("trimURLs")
|
||||
- ? lazy.BrowserUIUtils.trimURL(val)
|
||||
+ let trimmedValue = lazy.UrlbarPrefs.get("trimURLs") && this._zenTrimURL
|
||||
+ ? this._zenTrimURL(val)
|
||||
: val;
|
||||
- : val;
|
||||
+ let trimmedValue =
|
||||
+ UrlbarPrefs.get("trimURLs") && this._zenTrimURL
|
||||
+ ? this._zenTrimURL(val)
|
||||
+ : val;
|
||||
// Only trim value if the directionality doesn't change to RTL and we're not
|
||||
// showing a strikeout https protocol.
|
||||
@@ -4317,6 +4420,7 @@ ${
|
||||
browser = this.window.gBrowser.selectedBrowser,
|
||||
keepViewOpen = false
|
||||
) {
|
||||
+ openUILinkWhere = this.window.gZenUIManager.getOpenUILinkWhere(url, browser, openUILinkWhere);
|
||||
if (this.#isAddressbar) {
|
||||
this.#prepareAddressbarLoad(
|
||||
url,
|
||||
@@ -4430,6 +4534,10 @@ ${
|
||||
}
|
||||
reuseEmpty = true;
|
||||
}
|
||||
+ if (this.hasAttribute("zen-newtab")) {
|
||||
+ where = "tab";
|
||||
+ reuseEmpty = true;
|
||||
+ }
|
||||
if (
|
||||
where == "tab" &&
|
||||
reuseEmpty &&
|
||||
@@ -4437,6 +4545,9 @@ ${
|
||||
) {
|
||||
where = "current";
|
||||
}
|
||||
+ if (this.window.gBrowser.selectedTab.hasAttribute("zen-empty-tab")) {
|
||||
+ return "tab"; // Always open in a new tab if the current tab is "our empty tab".
|
||||
+ }
|
||||
return where;
|
||||
}
|
||||
|
||||
@@ -4691,6 +4802,7 @@ ${
|
||||
return this.controller.isTextDirectionRTL(trimmedValue) ||
|
||||
@@ -4235,6 +4339,11 @@ ${
|
||||
keepViewOpen = false,
|
||||
browserId = null,
|
||||
}) {
|
||||
+ where = this.window.gZenUIManager.getOpenUILinkWhere(
|
||||
+ url,
|
||||
+ this.window.gBrowser.selectedBrowser,
|
||||
+ where
|
||||
+ );
|
||||
let userTypedValue;
|
||||
if (this.#isAddressbar && where == "current") {
|
||||
// Make sure URL is formatted properly (don't show punycode).
|
||||
@@ -4576,6 +4685,7 @@ ${
|
||||
this.setResultForCurrentValue(null);
|
||||
this.handleCommand();
|
||||
this.controller.clearLastQueryContextCache();
|
||||
@@ -251,7 +237,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
|
||||
this._suppressStartQuery = false;
|
||||
});
|
||||
@@ -4698,7 +4810,6 @@ ${
|
||||
@@ -4583,7 +4693,6 @@ ${
|
||||
contextMenu.addEventListener("popupshowing", () => {
|
||||
// Close the results pane when the input field contextual menu is open,
|
||||
// because paste and go doesn't want a result selection.
|
||||
@@ -259,7 +245,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
|
||||
let controller =
|
||||
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
|
||||
@@ -4979,7 +5090,11 @@ ${
|
||||
@@ -4850,7 +4959,11 @@ ${
|
||||
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
||||
return;
|
||||
}
|
||||
@@ -272,7 +258,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
if (this._searchModeIndicatorTitle) {
|
||||
this._searchModeIndicatorTitle.textContent = "";
|
||||
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
||||
@@ -5297,6 +5412,7 @@ ${
|
||||
@@ -5149,6 +5262,7 @@ ${
|
||||
|
||||
this.document.l10n.setAttributes(
|
||||
this.inputField,
|
||||
@@ -280,7 +266,20 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
l10nId,
|
||||
l10nId == "urlbar-placeholder-with-name"
|
||||
? { name: engineName }
|
||||
@@ -5424,6 +5540,11 @@ ${
|
||||
@@ -5199,6 +5313,12 @@ ${
|
||||
}
|
||||
|
||||
lazy.logger.debug("Blur Event");
|
||||
+ if (
|
||||
+ this.document.commandDispatcher.focusedElement == this.inputField &&
|
||||
+ !UrlbarPrefs.get("closeOnWindowBlur")
|
||||
+ ) {
|
||||
+ return;
|
||||
+ }
|
||||
// We cannot count every blur events after a missed engagement as abandoment
|
||||
// because the user may have clicked on some view element that executes
|
||||
// a command causing a focus change. For example opening preferences from
|
||||
@@ -5276,6 +5396,11 @@ ${
|
||||
}
|
||||
|
||||
_on_click(event) {
|
||||
@@ -292,7 +291,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
switch (event.target) {
|
||||
case this.inputField:
|
||||
case this._inputContainer:
|
||||
@@ -5513,10 +5634,11 @@ ${
|
||||
@@ -5371,10 +5496,11 @@ ${
|
||||
}
|
||||
if (untrim) {
|
||||
this.setValue(this._untrimmedValue);
|
||||
@@ -305,7 +304,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
this.view.autoOpen({ event });
|
||||
} else {
|
||||
if (this._untrimOnFocusAfterKeydown) {
|
||||
@@ -5556,9 +5678,16 @@ ${
|
||||
@@ -5414,9 +5540,16 @@ ${
|
||||
}
|
||||
|
||||
_on_mousedown(event) {
|
||||
@@ -323,7 +322,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
if (
|
||||
event.composedTarget != this.inputField &&
|
||||
event.composedTarget != this._inputContainer
|
||||
@@ -5568,6 +5697,10 @@ ${
|
||||
@@ -5426,6 +5559,10 @@ ${
|
||||
|
||||
this.focusedViaMousedown = !this.focused;
|
||||
this.#preventClickSelectsAll = this.focused;
|
||||
@@ -334,7 +333,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
|
||||
// Keep the focus status, since the attribute may be changed
|
||||
// upon calling this.focus().
|
||||
@@ -5605,7 +5738,7 @@ ${
|
||||
@@ -5463,7 +5600,7 @@ ${
|
||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
||||
// view is already handled by the result opening flow.
|
||||
@@ -343,12 +342,12 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5894,7 +6027,7 @@ ${
|
||||
@@ -5754,7 +5891,7 @@ ${
|
||||
// When we are in actions search mode we can show more results so
|
||||
// increase the limit.
|
||||
let maxResults =
|
||||
- this.searchMode?.source != UrlbarShared.RESULT_SOURCE.ACTIONS
|
||||
+ this.searchMode?.source != UrlbarShared.RESULT_SOURCE.ZEN_ACTIONS
|
||||
? lazy.UrlbarPrefs.get("maxRichResults")
|
||||
? UrlbarPrefs.get("maxRichResults")
|
||||
: UNLIMITED_MAX_RESULTS;
|
||||
let options = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarShared.mjs b/browser/components/urlbar/content/UrlbarShared.mjs
|
||||
index 5b45afb747bfcdbd06782186737669aa2c4c634d..f68bd009f70415b5fb513abdfe19b4427ceefbfc 100644
|
||||
index 4e67dad66ae006842abc6eed547e4a244320d57a..393b61b4d2543c73e60afd334a353f36119ae73e 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarShared.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarShared.mjs
|
||||
@@ -47,6 +47,7 @@ export const UrlbarShared = {
|
||||
@@ -100,6 +100,7 @@ export const UrlbarShared = {
|
||||
// `looksLikeOrigin()` returned `LOOKS_LIKE_ORIGIN.OTHER` for this token.
|
||||
// It may or may not be an origin.
|
||||
POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED: 12,
|
||||
@@ -10,7 +10,7 @@ index 5b45afb747bfcdbd06782186737669aa2c4c634d..f68bd009f70415b5fb513abdfe19b442
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -67,6 +68,7 @@ export const UrlbarShared = {
|
||||
@@ -120,6 +121,7 @@ export const UrlbarShared = {
|
||||
TITLE: "#",
|
||||
URL: "$",
|
||||
ACTION: ">",
|
||||
@@ -18,7 +18,7 @@ index 5b45afb747bfcdbd06782186737669aa2c4c634d..f68bd009f70415b5fb513abdfe19b442
|
||||
}),
|
||||
|
||||
// Defines UrlbarResult types.
|
||||
@@ -112,6 +114,8 @@ export const UrlbarShared = {
|
||||
@@ -165,6 +167,8 @@ export const UrlbarShared = {
|
||||
OTHER_NETWORK: 6,
|
||||
ADDON: 7,
|
||||
ACTIONS: 8,
|
||||
@@ -26,9 +26,33 @@ index 5b45afb747bfcdbd06782186737669aa2c4c634d..f68bd009f70415b5fb513abdfe19b442
|
||||
+ WORKSPACES: 10,
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -128,6 +132,7 @@ export const UrlbarShared = {
|
||||
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
||||
// Results are categorized into groups to help the muxer compose them. See
|
||||
@@ -200,6 +204,8 @@ export const UrlbarShared = {
|
||||
SEMANTIC_HISTORY: "semanticHistory",
|
||||
SUGGESTED_INDEX: "suggestedIndex",
|
||||
TAIL_SUGGESTION: "tailSuggestion",
|
||||
+ ZEN_ACTION: "zenAction",
|
||||
+ ZEN_WORKSPACE: "zenWorkspace",
|
||||
}),
|
||||
|
||||
// Defines provider types.
|
||||
@@ -385,6 +391,14 @@ export const UrlbarShared = {
|
||||
telemetryLabel: "actions",
|
||||
uiLabel: "urlbar-searchmode-actions3",
|
||||
},
|
||||
+ {
|
||||
+ source: this.RESULT_SOURCE.WORKSPACES,
|
||||
+ restrict: this.RESTRICT_TOKENS.WORKSPACE,
|
||||
+ icon: "chrome://browser/skin/zen-icons/selectable/layers.svg",
|
||||
+ pref: "shortcuts.workspaces",
|
||||
+ telemetryLabel: "workspaces",
|
||||
+ uiLabel: "urlbar-search-mode-workspaces",
|
||||
+ },
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -402,6 +416,7 @@ export const UrlbarShared = {
|
||||
if (UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
||||
keys.push(this.RESTRICT_TOKENS.ACTION);
|
||||
}
|
||||
+ keys.push(this.RESTRICT_TOKENS.WORKSPACE);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
|
||||
index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac823fedcb 100644
|
||||
index 9a2c787cfa4d70bd587d49e017075f905c90f626..1c3b341e5d520ec9541c4021aaedce98751b6993 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarView.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarView.mjs
|
||||
@@ -674,7 +674,7 @@ export class UrlbarView {
|
||||
@@ -822,7 +822,7 @@ export class UrlbarView {
|
||||
!this.input.value ||
|
||||
this.input.getAttribute("pageproxystate") == "valid"
|
||||
) {
|
||||
@@ -11,7 +11,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
||||
// Try to reuse the cached top-sites context. If it's not cached, then
|
||||
// there will be a gap of time between when the input is focused and
|
||||
// when the view opens that can be perceived as flicker.
|
||||
@@ -812,10 +812,6 @@ export class UrlbarView {
|
||||
@@ -963,10 +963,6 @@ export class UrlbarView {
|
||||
}
|
||||
|
||||
// If search mode isn't active, close the view.
|
||||
@@ -22,7 +22,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
||||
|
||||
// Search mode is active. If the one-offs should be shown, make sure they
|
||||
// are enabled and show the view.
|
||||
@@ -3031,6 +3027,8 @@ export class UrlbarView {
|
||||
@@ -3306,6 +3302,8 @@ export class UrlbarView {
|
||||
if (row?.hasAttribute("row-selectable")) {
|
||||
row?.toggleAttribute("selected", true);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
||||
if (element != row) {
|
||||
row?.toggleAttribute("descendant-selected", true);
|
||||
}
|
||||
@@ -3544,7 +3542,7 @@ export class UrlbarView {
|
||||
@@ -3972,7 +3970,7 @@ export class UrlbarView {
|
||||
}
|
||||
|
||||
#enableOrDisableRowWrap() {
|
||||
@@ -40,3 +40,16 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
||||
this.#rows.toggleAttribute("wrap", wrap);
|
||||
this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap);
|
||||
}
|
||||
@@ -4387,6 +4385,12 @@ export class UrlbarView {
|
||||
}
|
||||
|
||||
on_blur() {
|
||||
+ if (
|
||||
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
|
||||
+ !UrlbarPrefs.get("closeOnWindowBlur")
|
||||
+ ) {
|
||||
+ return;
|
||||
+ }
|
||||
// If the view is open without the input being focused, it will not close
|
||||
// automatically when the window loses focus. We might be in this state
|
||||
// after a Search Tip is shown on an engine homepage.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
|
||||
index e735ec80b56e1a2da4bc5bec29136ce60353277b..330ea62d220d9acd96a95a605888839388e97452 100644
|
||||
index f276db1d456703493ee713370691352985021708..619ee6724eec1e6a25adcb257af0d02eb7a29d34 100644
|
||||
--- a/browser/themes/linux/browser.css
|
||||
+++ b/browser/themes/linux/browser.css
|
||||
@@ -14,7 +14,6 @@
|
||||
@media (-moz-gtk-theme-family) {
|
||||
--tabs-navbar-separator-style: none;
|
||||
@media (prefers-color-scheme: light) and (not -moz-pref("browser.nova.enabled")) {
|
||||
@media (prefers-color-scheme: light) {
|
||||
- --urlbar-box-background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
diff --git a/browser/themes/shared/tabbrowser/content-area.css b/browser/themes/shared/tabbrowser/content-area.css
|
||||
index 3acdec2b8e043fe27919cfa54e73f4d3c491bfc8..267a1087134464a8193a824a2d7f8ac15ff8ad60 100644
|
||||
index 2374ed38bd9361d6c7e1886117099c6761e403b8..72f1f52a13096f19ca4009037ad0cdc880775ff7 100644
|
||||
--- a/browser/themes/shared/tabbrowser/content-area.css
|
||||
+++ b/browser/themes/shared/tabbrowser/content-area.css
|
||||
@@ -157,7 +157,6 @@
|
||||
min-height: 0;
|
||||
@@ -239,7 +239,6 @@
|
||||
min-width: 0;
|
||||
|
||||
/* We want to be able to show the frame color behind the clipped radiused corner */
|
||||
- background: var(--tabpanel-background-color);
|
||||
|
||||
@media -moz-pref("sidebar.revamp") {
|
||||
outline: 0.01px solid var(--chrome-content-separator-color);
|
||||
@@ -216,7 +215,6 @@
|
||||
display: grid;
|
||||
/**
|
||||
@@ -425,7 +424,6 @@
|
||||
}
|
||||
|
||||
browser:is([blank], [pendingpaint]) {
|
||||
@@ -18,7 +18,7 @@ index 3acdec2b8e043fe27919cfa54e73f4d3c491bfc8..267a1087134464a8193a824a2d7f8ac1
|
||||
}
|
||||
|
||||
/* Exclude browsers with smartwindow-content attribute which inherit
|
||||
@@ -553,7 +551,7 @@ split-view-footer {
|
||||
@@ -865,7 +863,7 @@ split-view-footer {
|
||||
|
||||
.dialogStack {
|
||||
z-index: var(--browser-stack-z-index-dialog-stack);
|
||||
@@ -27,12 +27,12 @@ index 3acdec2b8e043fe27919cfa54e73f4d3c491bfc8..267a1087134464a8193a824a2d7f8ac1
|
||||
inset: 0;
|
||||
/* --browser-with-dialog set on browser[tabDialogShowing], we want to position the overlay
|
||||
only on the top of the <browser> element so it doesn't overlap the DevTools toolbox */
|
||||
@@ -718,7 +716,7 @@ split-view-footer {
|
||||
@@ -1034,7 +1032,7 @@ split-view-footer {
|
||||
|
||||
.dialogOverlay[topmost="true"],
|
||||
#window-modal-dialog::backdrop {
|
||||
- background-color: var(--background-color-overlay);
|
||||
+ background-color: light-dark(rgba(255, 255, 255, .3), rgba(0, 0, 0, .3));
|
||||
}
|
||||
border-radius: var(--chrome-block-radius);
|
||||
|
||||
.dialogOverlay[hideContent="true"][topmost="true"] {
|
||||
.browserSidebarContainer.responsive-mode & {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
diff --git a/browser/themes/shared/tabbrowser/tabs.css b/browser/themes/shared/tabbrowser/tabs.css
|
||||
index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f253804576e 100644
|
||||
index 61c0b8382c78f63ad9113af55a6236c4a29bc45b..0ab463c768d4fb3f45d22e753f408c46b61c0480 100644
|
||||
--- a/browser/themes/shared/tabbrowser/tabs.css
|
||||
+++ b/browser/themes/shared/tabbrowser/tabs.css
|
||||
@@ -45,7 +45,7 @@
|
||||
@@ -46,7 +46,7 @@
|
||||
--tab-group-line-thickness: 2px;
|
||||
--tab-group-line-toolbar-border-distance: 1px;
|
||||
/* Collapsed tabs should be square, so set width to match the min height */
|
||||
- --tab-collapsed-background-width: var(--tab-min-height);
|
||||
+ --tab-collapsed-background-width: 48px;
|
||||
--tab-collapsed-width: calc(var(--tab-collapsed-background-width) + 2 * var(--tab-inner-inline-margin));
|
||||
--tab-collapsed-split-pill-offset: 0px;
|
||||
--tab-pinned-min-width-expanded: calc(var(--tab-pinned-expanded-background-width) + 2 * var(--tab-pinned-margin-inline-expanded));
|
||||
--tab-note-icon-end-margin: var(--dimension-4);
|
||||
@@ -295,7 +295,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -308,7 +308,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
}
|
||||
|
||||
:root:not([uidensity="compact"], [sidebar-expand-on-hover]) &[pinned] {
|
||||
@@ -19,7 +19,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
}
|
||||
|
||||
&:is([selected], [multiselected]) {
|
||||
@@ -309,6 +308,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -322,6 +321,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
border-radius: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -27,7 +27,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
@@ -506,10 +506,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -519,10 +519,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
|
||||
@media -moz-pref("browser.tabs.fadeOutUnloadedTabs") {
|
||||
&[pending] {
|
||||
@@ -38,7 +38,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
opacity: 0.5;
|
||||
/* Fade the favicon out */
|
||||
transition-property: filter, opacity;
|
||||
@@ -525,10 +521,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -538,10 +534,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
|
||||
@media -moz-pref("browser.tabs.fadeOutExplicitlyUnloadedTabs") {
|
||||
&[pending][discarded] {
|
||||
@@ -49,7 +49,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
opacity: 0.5;
|
||||
/* Fade the favicon out */
|
||||
transition-property: filter, opacity;
|
||||
@@ -602,7 +594,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -615,7 +607,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
}
|
||||
|
||||
#tabbrowser-tabs[orient="vertical"] & {
|
||||
@@ -58,7 +58,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
}
|
||||
|
||||
&[crashed] {
|
||||
@@ -610,7 +602,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -623,7 +615,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
}
|
||||
|
||||
#tabbrowser-tabs[orient="vertical"]:not([expanded]) &:not([crashed]),
|
||||
@@ -67,7 +67,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
&[soundplaying] {
|
||||
list-style-image: url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg");
|
||||
}
|
||||
@@ -664,7 +656,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -677,7 +669,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
&[crashed] {
|
||||
display: revert;
|
||||
}
|
||||
@@ -824,7 +816,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -837,7 +829,7 @@ tab-split-view-wrapper[dragtarget] {
|
||||
has not been added to root. There are certain scenarios when that attribute is temporarily
|
||||
removed from root such as when toggling the sidebar to expand with the toolbar button. */
|
||||
#tabbrowser-tabs[orient="horizontal"] &:not([pinned]):not([crashed]),
|
||||
@@ -85,7 +85,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
&:is([soundplaying], [muted], [activemedia-blocked]) {
|
||||
display: flex;
|
||||
}
|
||||
@@ -1044,7 +1036,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
@@ -1151,7 +1143,6 @@ tab-split-view-wrapper[dragtarget] {
|
||||
.tabbrowser-tab:is([image], [pinned]) > .tab-stack > .tab-content[attention]:not([selected]),
|
||||
.tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected]),
|
||||
#tabbrowser-tabs[orient="vertical"] .tabbrowser-tab > .tab-stack > .tab-content[titlechanged]:not([selected]) {
|
||||
@@ -93,7 +93,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
background-position: center bottom 6.5px;
|
||||
background-size: 4px 4px;
|
||||
background-repeat: no-repeat;
|
||||
@@ -1797,7 +1788,7 @@ tab-group {
|
||||
@@ -1904,7 +1895,7 @@ tab-group {
|
||||
}
|
||||
|
||||
#tabbrowser-tabs[orient="vertical"][expanded] {
|
||||
@@ -102,7 +102,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
&[movingtab][movingtab-addToGroup]:not([movingtab-group], [movingtab-ungroup]) .tabbrowser-tab:is(:active, [multiselected]) {
|
||||
margin-inline-start: var(--space-medium);
|
||||
}
|
||||
@@ -2351,7 +2342,7 @@ tab-group {
|
||||
@@ -2504,7 +2495,7 @@ tab-group {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
#vertical-tabs-newtab-button {
|
||||
appearance: none;
|
||||
min-height: var(--tab-min-height);
|
||||
@@ -2362,7 +2353,7 @@ tab-group {
|
||||
margin-inline: var(--tab-inner-inline-margin);
|
||||
@@ -2524,7 +2515,7 @@ tab-group {
|
||||
}
|
||||
|
||||
#tabbrowser-tabs[orient="vertical"]:not([expanded]) & > .toolbarbutton-text {
|
||||
- display: none;
|
||||
@@ -120,7 +120,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -2386,7 +2377,7 @@ tab-group {
|
||||
@@ -2548,7 +2539,7 @@ tab-group {
|
||||
* flex container. #tabs-newtab-button is a child of the arrowscrollbox where
|
||||
* we don't want a gap (between tabs), so we have to add some margin.
|
||||
*/
|
||||
@@ -129,7 +129,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
margin-block: var(--tab-margin-block);
|
||||
}
|
||||
|
||||
@@ -2576,7 +2567,6 @@ tab-group {
|
||||
@@ -2740,7 +2731,6 @@ tab-group {
|
||||
|
||||
&:not([expanded]) {
|
||||
.tabbrowser-tab[pinned] {
|
||||
@@ -137,7 +137,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
@@ -2615,8 +2605,8 @@ tab-group {
|
||||
@@ -2779,8 +2769,8 @@ tab-group {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: auto;
|
||||
@@ -148,7 +148,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
|
||||
&:-moz-window-inactive {
|
||||
background-image:
|
||||
@@ -2714,9 +2704,6 @@ tab-group {
|
||||
@@ -2878,9 +2868,6 @@ tab-group {
|
||||
~ #tabbrowser-tabs[orient="horizontal"]::before {
|
||||
display: flex;
|
||||
content: "";
|
||||
@@ -158,7 +158,7 @@ index 0820fa040642ee05c2a24f5d2cfa58da5c926647..2d93abcce1729744e5963f1c6ad47f25
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2749,7 +2736,6 @@ toolbar:not(#TabsToolbar) #firefox-view-button {
|
||||
@@ -2913,7 +2900,6 @@ toolbar:not(#TabsToolbar) #firefox-view-button {
|
||||
list-style-image: url(chrome://global/skin/icons/plus.svg);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
diff --git a/browser/themes/shared/urlbar-searchbar.css b/browser/themes/shared/urlbar-searchbar.css
|
||||
index 22d624d718a289dd7b84ffc2f04dd35893dfbe95..4f57f7853f3043e3ad01d893aa928b9d2e6f3c16 100644
|
||||
index 149f123e26cd79e89432dfeb504ca7218ee04810..7e0e6d981d6c2aa53bc2a76d82d2078419659065 100644
|
||||
--- a/browser/themes/shared/urlbar-searchbar.css
|
||||
+++ b/browser/themes/shared/urlbar-searchbar.css
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
@media not -moz-pref("browser.nova.enabled") {
|
||||
--urlbar-basic-component-size: calc(var(--urlbar-min-height) - 2 * (var(--urlbar-input-container-border-width) + var(--urlbar-input-container-padding)));
|
||||
- --urlbar-icon-padding: calc((var(--urlbar-min-height) - 2px /* border */ - 2px /* padding */ - 16px /* icon */) / 2);
|
||||
--urlbar-basic-component-size: calc(var(--urlbar-height) - 2 * (var(--urlbar-input-container-border-width) + var(--urlbar-input-container-padding)));
|
||||
- --urlbar-icon-padding: calc((var(--urlbar-height) - 2px /* border */ - 2px /* padding */ - 16px /* icon */) / 2);
|
||||
- --urlbar-input-container-padding: round(up, 1px, env(hairline));
|
||||
+ --urlbar-icon-padding: calc((var(--urlbar-min-height) - 2px /* border */ - 14px /* icon */) / 2);
|
||||
+ --urlbar-icon-padding: calc((var(--urlbar-height) - 2px /* border */ - 14px /* icon */) / 2);
|
||||
+ --urlbar-input-container-padding: 2px;
|
||||
}
|
||||
@media (max-width: 770px) {
|
||||
--urlbar-container-min-width: 240px;
|
||||
@@ -126,7 +126,7 @@ toolbar[inactive="true"] .urlbar,
|
||||
@@ -127,7 +127,7 @@ toolbar[inactive="true"] .urlbar,
|
||||
.urlbar:not([usertyping]) > .urlbar-input-container > .urlbar-go-button,
|
||||
.urlbar:not(#searchbar-new, [focused]) > .urlbar-input-container > .urlbar-go-button,
|
||||
#urlbar-revert-button-container {
|
||||
@@ -22,7 +22,7 @@ index 22d624d718a289dd7b84ffc2f04dd35893dfbe95..4f57f7853f3043e3ad01d893aa928b9d
|
||||
}
|
||||
|
||||
/* Document Picture-in-Picture API window */
|
||||
@@ -403,6 +403,10 @@ toolbar[inactive="true"] .urlbar,
|
||||
@@ -405,6 +405,10 @@ toolbar[inactive="true"] .urlbar,
|
||||
.urlbar:not([focused])[textoverflow="left"][domaindir="rtl"] > .urlbar-input-container > .urlbar-input-box > & {
|
||||
mask-image: linear-gradient(to right, transparent var(--urlbar-scheme-size), black calc(var(--urlbar-scheme-size) + 3ch));
|
||||
}
|
||||
@@ -33,16 +33,18 @@ index 22d624d718a289dd7b84ffc2f04dd35893dfbe95..4f57f7853f3043e3ad01d893aa928b9d
|
||||
}
|
||||
|
||||
#urlbar-scheme {
|
||||
@@ -492,10 +496,14 @@ toolbar[inactive="true"] .urlbar,
|
||||
@@ -476,10 +480,15 @@ toolbar[inactive="true"] .urlbar,
|
||||
}
|
||||
|
||||
.urlbar[breakout][breakout-extend] {
|
||||
height: auto;
|
||||
- margin-left: calc(-1 * var(--urlbar-margin-inline));
|
||||
+ align-items: center;
|
||||
+ :root:not([zen-single-toolbar='true']) & {
|
||||
margin-left: calc(-1 * var(--urlbar-margin-inline));
|
||||
+ }
|
||||
width: calc(var(--urlbar-width) + 2 * var(--urlbar-margin-inline));
|
||||
|
||||
+ :root:not([zen-single-toolbar="true"]) & {
|
||||
+ margin-left: calc(-1 * var(--urlbar-margin-inline));
|
||||
+ }
|
||||
+
|
||||
> .urlbar-input-container {
|
||||
+ align-items: center;
|
||||
height: var(--urlbar-container-height);
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
|
||||
--- a/browser/app/profile/firefox.js
|
||||
+++ b/browser/app/profile/firefox.js
|
||||
@@ -411,10 +411,13 @@
|
||||
pref("browser.urlbar.maxRichResults", 10);
|
||||
|
||||
// The maximum number of historical search results to show.
|
||||
pref("browser.urlbar.maxHistoricalSearchSuggestions", 2);
|
||||
|
||||
+// Whether to close the urlbar when blurring the window while the urlbar is focused.
|
||||
+pref("browser.urlbar.closeOnWindowBlur", true);
|
||||
+
|
||||
// The default behavior for the urlbar can be configured to use any combination
|
||||
// of the match filters with each additional filter adding more results (union).
|
||||
pref("browser.urlbar.suggest.bookmark", true);
|
||||
pref("browser.urlbar.suggest.clipboard", true);
|
||||
pref("browser.urlbar.suggest.history", true);
|
||||
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||
@@ -152,10 +152,13 @@
|
||||
["filter.javascript", true],
|
||||
|
||||
// Feature gate pref for flight status suggestions in the urlbar.
|
||||
["flightStatus.featureGate", false],
|
||||
|
||||
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
|
||||
+ ["closeOnWindowBlur", true],
|
||||
+
|
||||
// The minimum prefix length of a flight status keyword the user must type to
|
||||
// trigger the suggestion. 0 means the min length should be taken from Nimbus
|
||||
// or remote settings.
|
||||
["flightStatus.minKeywordLength", 0],
|
||||
|
||||
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
|
||||
--- a/browser/components/urlbar/content/UrlbarView.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarView.mjs
|
||||
@@ -3909,10 +3909,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
on_blur() {
|
||||
+ if (
|
||||
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
|
||||
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
||||
+ ) {
|
||||
+ return;
|
||||
+ }
|
||||
// If the view is open without the input being focused, it will not close
|
||||
// automatically when the window loses focus. We might be in this state
|
||||
// after a Search Tip is shown on an engine homepage.
|
||||
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
|
||||
this.close();
|
||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
@@ -5460,11 +5460,17 @@
|
||||
_on_blur(event) {
|
||||
if (this.view.resultMenu.hasAttribute("open")) {
|
||||
return;
|
||||
}
|
||||
|
||||
lazy.logger.debug("Blur Event");
|
||||
+ if (
|
||||
+ this.document.commandDispatcher.focusedElement == this.inputField &&
|
||||
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
||||
+ ) {
|
||||
+ return;
|
||||
+ }
|
||||
// We cannot count every blur events after a missed engagement as abandoment
|
||||
// because the user may have clicked on some view element that executes
|
||||
// a command causing a focus change. For example opening preferences from
|
||||
// the oneoff settings button.
|
||||
// For now we detect that case by discarding the event on command, but we
|
||||
// may want to figure out a more robust way to detect abandonment.
|
||||
@@ -31,12 +31,13 @@ diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src
|
||||
|
||||
// Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
|
||||
set_profiler_hooks(Some(&PROFILER_HOOKS));
|
||||
@@ -2164,10 +2165,11 @@
|
||||
@@ -2164,11 +2165,12 @@
|
||||
texture_cache_config,
|
||||
reject_software_rasterizer,
|
||||
low_quality_pinch_zoom,
|
||||
max_shared_surface_size,
|
||||
enable_dithering,
|
||||
enable_shared_instance_buffer,
|
||||
+ opaque_backdrop_fallback,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -73,8 +74,9 @@ diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/render
|
||||
+ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
|
||||
+ pub opaque_backdrop_fallback: bool,
|
||||
}
|
||||
@@ -277,9 +279,10 @@
|
||||
@@ -277,10 +279,11 @@
|
||||
enable_instancing: true,
|
||||
enable_shared_instance_buffer: false,
|
||||
reject_software_rasterizer: false,
|
||||
low_quality_pinch_zoom: false,
|
||||
max_shared_surface_size: 2048,
|
||||
@@ -84,12 +86,13 @@ diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/render
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,10 +805,11 @@
|
||||
@@ -802,11 +805,12 @@
|
||||
allocated_native_surfaces: FastHashSet::default(),
|
||||
debug_overlay_state: DebugOverlayState::new(),
|
||||
buffer_damage_tracker: BufferDamageTracker::default(),
|
||||
max_primitive_instance_count,
|
||||
enable_instancing: options.enable_instancing,
|
||||
use_shared_instance_buffer,
|
||||
+ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
|
||||
consecutive_oom_frames: 0,
|
||||
target_frame_publish_id: None,
|
||||
@@ -99,11 +102,14 @@ diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/render
|
||||
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
|
||||
--- a/gfx/wr/webrender/src/renderer/mod.rs
|
||||
+++ b/gfx/wr/webrender/src/renderer/mod.rs
|
||||
@@ -867,10 +867,12 @@
|
||||
@@ -867,13 +867,15 @@
|
||||
buffer_damage_tracker: BufferDamageTracker,
|
||||
|
||||
max_primitive_instance_count: usize,
|
||||
enable_instancing: bool,
|
||||
/// If true, stream instance data into large buffers shared by multiple
|
||||
/// draws, rather than allocating a new instance buffer for each draw.
|
||||
use_shared_instance_buffer: bool,
|
||||
|
||||
+ opaque_backdrop_fallback: bool,
|
||||
+
|
||||
|
||||
168
src/external-patches/firefox/bug_2011236.patch
Normal file
168
src/external-patches/firefox/bug_2011236.patch
Normal file
@@ -0,0 +1,168 @@
|
||||
diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml
|
||||
--- a/browser/components/sessionstore/test/browser.toml
|
||||
+++ b/browser/components/sessionstore/test/browser.toml
|
||||
@@ -14,10 +14,12 @@
|
||||
"browser_frame_history_c.html",
|
||||
"browser_frame_history_c1.html",
|
||||
"browser_frame_history_c2.html",
|
||||
"browser_formdata_format_sample.html",
|
||||
"browser_sessionHistory_slow.sjs",
|
||||
+ "browser_policy_container_sample.html",
|
||||
+ "browser_policy_container_sample_frame.html",
|
||||
"browser_scrollPositions_sample.html",
|
||||
"browser_scrollPositions_sample2.html",
|
||||
"browser_scrollPositions_sample_frameset.html",
|
||||
"browser_scrollPositions_readerModeArticle.html",
|
||||
"browser_sessionStorage.html",
|
||||
@@ -226,10 +228,12 @@
|
||||
["browser_pinned_tabs.js"]
|
||||
skip-if = [
|
||||
"ccov", # Bug 1625525
|
||||
]
|
||||
|
||||
+["browser_policy_container_not_stored.js"]
|
||||
+
|
||||
["browser_privatetabs.js"]
|
||||
|
||||
["browser_purge_domaindata.js"]
|
||||
|
||||
["browser_purge_shistory.js"]
|
||||
diff --git a/browser/components/sessionstore/test/browser_policy_container_not_stored.js b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
|
||||
@@ -0,0 +1,51 @@
|
||||
+/* 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/. */
|
||||
+
|
||||
+"use strict";
|
||||
+
|
||||
+/**
|
||||
+ * A session history entry only needs to retain the policy container for loads
|
||||
+ * that inherit their policies (about:blank, about:srcdoc, blob:, data:, ...).
|
||||
+ * Anything fetched over the network gets its policies from the response again,
|
||||
+ * so storing them would only bloat the session store (bug 2011236).
|
||||
+ */
|
||||
+
|
||||
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
|
||||
+ "chrome://mochitests/content",
|
||||
+ "https://example.com"
|
||||
+);
|
||||
+const PARENT_URL = TEST_ROOT + "browser_policy_container_sample.html";
|
||||
+const FRAME_URL = TEST_ROOT + "browser_policy_container_sample_frame.html";
|
||||
+
|
||||
+add_task(async function test_policy_container_only_for_inheriting_loads() {
|
||||
+ const tab = BrowserTestUtils.addTab(gBrowser, PARENT_URL);
|
||||
+ gBrowser.selectedTab = tab;
|
||||
+ await promiseBrowserLoaded(tab.linkedBrowser, true, PARENT_URL);
|
||||
+ await TabStateFlusher.flush(tab.linkedBrowser);
|
||||
+
|
||||
+ const state = JSON.parse(ss.getTabState(tab));
|
||||
+ const topEntry = state.entries.at(-1);
|
||||
+ is(topEntry.url, PARENT_URL, "collected the parent entry");
|
||||
+
|
||||
+ const children = topEntry.children ?? [];
|
||||
+ const networkFrame = children.find(child => child.url === FRAME_URL);
|
||||
+ const blankFrame = children.find(child => child.url === "about:blank");
|
||||
+
|
||||
+ ok(networkFrame, "collected the network-scheme subframe entry");
|
||||
+ ok(blankFrame, "collected the about:blank subframe entry");
|
||||
+
|
||||
+ // The parent has a CSP, so both subframes inherit a policy container onto
|
||||
+ // their load state. Only the one that cannot recover it from a response
|
||||
+ // should keep it in session history.
|
||||
+ ok(
|
||||
+ !("policyContainer" in networkFrame),
|
||||
+ "https subframe entry does not store a policyContainer"
|
||||
+ );
|
||||
+ ok(
|
||||
+ "policyContainer" in blankFrame,
|
||||
+ "about:blank subframe entry still stores its inherited policyContainer"
|
||||
+ );
|
||||
+
|
||||
+ await BrowserTestUtils.removeTab(tab);
|
||||
+});
|
||||
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample.html b/browser/components/sessionstore/test/browser_policy_container_sample.html
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/browser/components/sessionstore/test/browser_policy_container_sample.html
|
||||
@@ -0,0 +1,12 @@
|
||||
+<!DOCTYPE html>
|
||||
+<html>
|
||||
+<head>
|
||||
+ <meta charset="utf-8">
|
||||
+ <meta http-equiv="Content-Security-Policy" content="default-src * data: blob: 'unsafe-inline'">
|
||||
+ <title>policyContainer session store sample</title>
|
||||
+</head>
|
||||
+<body>
|
||||
+ <iframe id="network" src="browser_policy_container_sample_frame.html"></iframe>
|
||||
+ <iframe id="blank" src="about:blank"></iframe>
|
||||
+</body>
|
||||
+</html>
|
||||
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample_frame.html b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
|
||||
@@ -0,0 +1,10 @@
|
||||
+<!DOCTYPE html>
|
||||
+<html>
|
||||
+<head>
|
||||
+ <meta charset="utf-8">
|
||||
+ <title>policyContainer session store sample frame</title>
|
||||
+</head>
|
||||
+<body>
|
||||
+ frame
|
||||
+</body>
|
||||
+</html>
|
||||
diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp
|
||||
--- a/docshell/shistory/SessionHistoryEntry.cpp
|
||||
+++ b/docshell/shistory/SessionHistoryEntry.cpp
|
||||
@@ -41,10 +41,18 @@
|
||||
extern mozilla::LazyLogModule gSHLog;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
+// Only store policy container for loads that can't carry it themselves
|
||||
+// (about:blank, about:srcdoc, blob:, data:, ...)
|
||||
+// bug 1867137, bug 2011236
|
||||
+static nsIPolicyContainer* PolicyContainerToStore(
|
||||
+ nsIURI* aURI, nsIPolicyContainer* aPolicyContainer) {
|
||||
+ return CSP_ShouldURIInheritCSP(aURI) ? aPolicyContainer : nullptr;
|
||||
+}
|
||||
+
|
||||
SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState,
|
||||
nsIChannel* aChannel)
|
||||
: mURI(aLoadState->URI()),
|
||||
mOriginalURI(aLoadState->OriginalURI()),
|
||||
mResultPrincipalURI(aLoadState->ResultPrincipalURI()),
|
||||
@@ -59,11 +67,12 @@
|
||||
mLoadReplace(aLoadState->LoadReplace()),
|
||||
mHasUserActivation(aLoadState->HasValidUserGestureActivation()),
|
||||
mSharedState(SharedState::Create(
|
||||
aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(),
|
||||
aLoadState->PartitionedPrincipalToInherit(),
|
||||
- aLoadState->PolicyContainer(),
|
||||
+ PolicyContainerToStore(aLoadState->URI(),
|
||||
+ aLoadState->PolicyContainer()),
|
||||
/* FIXME Is this correct? */
|
||||
aLoadState->TypeHint())) {
|
||||
MOZ_DIAGNOSTIC_ASSERT(!mURI->SchemeIs("javascript"));
|
||||
|
||||
// Pull the upload stream off of the channel instead of the load state, as
|
||||
@@ -124,11 +133,12 @@
|
||||
loadInfo->GetPrincipalToInherit(
|
||||
getter_AddRefs(mSharedState.Get()->mPrincipalToInherit));
|
||||
|
||||
mSharedState.Get()->mPartitionedPrincipalToInherit =
|
||||
aPartitionedPrincipalToInherit;
|
||||
- mSharedState.Get()->mPolicyContainer = aPolicyContainer;
|
||||
+ mSharedState.Get()->mPolicyContainer =
|
||||
+ PolicyContainerToStore(mURI, aPolicyContainer);
|
||||
aChannel->GetContentType(mSharedState.Get()->mContentType);
|
||||
aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI));
|
||||
|
||||
uint32_t loadFlags;
|
||||
aChannel->GetLoadFlags(&loadFlags);
|
||||
|
||||
2121
src/external-patches/firefox/css_corner_shape_rendering.patch
Normal file
2121
src/external-patches/firefox/css_corner_shape_rendering.patch
Normal file
File diff suppressed because it is too large
Load Diff
55
src/external-patches/firefox/issue_14710.patch
Normal file
55
src/external-patches/firefox/issue_14710.patch
Normal file
@@ -0,0 +1,55 @@
|
||||
diff --git a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||
--- a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||
+++ b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||
@@ -119,10 +119,19 @@
|
||||
}
|
||||
case "PictureInPicture:VideoTabHidden": {
|
||||
if (!lazy.PIP_ENABLED || !lazy.PIP_WHEN_SWITCHING_TABS) {
|
||||
break;
|
||||
}
|
||||
+
|
||||
+ // Suppress auto-toggle if the user deliberately closed this PiP window
|
||||
+ if (
|
||||
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.has(browser)
|
||||
+ ) {
|
||||
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.delete(browser);
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
// If the tab is still selected, then we can ignore this event
|
||||
if (browser.documentGlobal.gBrowser.selectedBrowser == browser) {
|
||||
break;
|
||||
}
|
||||
let actor = browsingContext.currentWindowGlobal.getActor(
|
||||
@@ -261,10 +270,12 @@
|
||||
maxConcurrentPlayerCount: 0,
|
||||
|
||||
// Maps auto pip browser to PictureInPictureParent actor
|
||||
weakAutoPipBrowserToParent: new WeakMap(),
|
||||
|
||||
+ weakAutoPipBrowserClosedDeliberately: new WeakSet(),
|
||||
+
|
||||
/**
|
||||
* Returns the player window if one exists and if it hasn't yet been closed.
|
||||
*
|
||||
* @param {PictureInPictureParent} pipActorRef
|
||||
* Reference to the calling PictureInPictureParent actor
|
||||
@@ -899,10 +910,17 @@
|
||||
const { reason, actorRef } = closeData;
|
||||
const win = this.getWeakPipPlayer(actorRef);
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
+
|
||||
+ const browser = this.weakWinToBrowser.get(win);
|
||||
+
|
||||
+ if (reason === "CloseButton") {
|
||||
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.add(browser);
|
||||
+ }
|
||||
+
|
||||
this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win));
|
||||
this.weakAutoPipBrowserToParent.delete(this.weakWinToBrowser.get(win));
|
||||
|
||||
Glean.pictureinpicture["closedMethod" + reason].record();
|
||||
await this.closePipWindow(win);
|
||||
|
||||
@@ -7,14 +7,6 @@
|
||||
"id": "D299584",
|
||||
"name": "Native MacOS popovers fix"
|
||||
},
|
||||
{
|
||||
"type": "phabricator",
|
||||
"id": "D284404",
|
||||
"name": "Add urlbar closeOnWindowBlur preference",
|
||||
"replaces": {
|
||||
"\n\n": "\n // may want to figure out a more robust way to detect abandonment."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "local",
|
||||
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
|
||||
@@ -48,5 +40,19 @@
|
||||
"this._lastJsonLength = uncompressedBytes;": "this._lastJsonLength = uncompressedBytes.jsonLength;",
|
||||
" jsonLengthHint,": " lengthHint: jsonLengthHint,"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "phabricator",
|
||||
"id": "D320897",
|
||||
"name": "Bug 2011236"
|
||||
},
|
||||
{
|
||||
"type": "phabricator",
|
||||
"id": "D321446",
|
||||
"name": "Issue 14710"
|
||||
},
|
||||
{
|
||||
"type": "local",
|
||||
"path": "firefox/css_corner_shape_rendering.patch"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
diff --git a/layout/style/StyleColor.cpp b/layout/style/StyleColor.cpp
|
||||
index bbee32af30f35525730a10d0b242c0e8a8fe4695..a590fff105b30bc96fa7a0d8a8d3b1d5aa997e08 100644
|
||||
index f922f7fdacc7e1a14beb8da77b48aef3f6ae0b2b..47d19242baaf2a1f25ee07c6e210c839cc7b5089 100644
|
||||
--- a/layout/style/StyleColor.cpp
|
||||
+++ b/layout/style/StyleColor.cpp
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "mozilla/StyleColorInlines.h"
|
||||
#include "nsIFrame.h"
|
||||
#include "nsStyleStruct.h"
|
||||
+#include "mozilla/nsZenBoostsBackend.h"
|
||||
#include "nsStyleUtil.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
@@ -96,23 +97,24 @@ StyleAbsoluteColor StyleColor::ResolveColor(
|
||||
@@ -98,23 +99,24 @@ StyleAbsoluteColor StyleColor::ResolveColor(
|
||||
|
||||
template <>
|
||||
nscolor StyleColor::CalcColor(nscolor aColor) const {
|
||||
@@ -40,7 +40,7 @@ index bbee32af30f35525730a10d0b242c0e8a8fe4695..a590fff105b30bc96fa7a0d8a8d3b1d5
|
||||
}
|
||||
|
||||
StyleAbsoluteColor StyleAbsoluteColor::ToColorSpace(
|
||||
@@ -120,7 +122,7 @@ StyleAbsoluteColor StyleAbsoluteColor::ToColorSpace(
|
||||
@@ -122,7 +124,7 @@ StyleAbsoluteColor StyleAbsoluteColor::ToColorSpace(
|
||||
return Servo_ConvertColorSpace(this, aColorSpace);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ index bbee32af30f35525730a10d0b242c0e8a8fe4695..a590fff105b30bc96fa7a0d8a8d3b1d5
|
||||
constexpr StyleColorSpace DEST_COLOR_SPACE = StyleColorSpace::Srgb;
|
||||
|
||||
constexpr float MIN = 0.0f;
|
||||
@@ -162,11 +164,13 @@ nscolor StyleAbsoluteColor::ToColor() const {
|
||||
@@ -164,11 +166,13 @@ nscolor StyleAbsoluteColor::ToColor() const {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/layout/svg/SVGImageContext.h b/layout/svg/SVGImageContext.h
|
||||
index b1f811989697966e30859610f4fc07579d0385ab..f6f4cd5a45fc49b416111d980fab4e612788903e 100644
|
||||
index ad833c7c2ee6a4c4984efdb0d59e3576c2d1bc19..36864a34badc0cbfe44d6295798622fd6178f9e3 100644
|
||||
--- a/layout/svg/SVGImageContext.h
|
||||
+++ b/layout/svg/SVGImageContext.h
|
||||
@@ -6,6 +6,7 @@
|
||||
@@ -44,7 +44,7 @@ index b1f811989697966e30859610f4fc07579d0385ab..f6f4cd5a45fc49b416111d980fab4e61
|
||||
const Maybe<SVGPreserveAspectRatio>& GetPreserveAspectRatio() const {
|
||||
return mPreserveAspectRatio;
|
||||
}
|
||||
@@ -116,7 +137,11 @@ class SVGImageContext {
|
||||
@@ -110,7 +131,11 @@ class SVGImageContext {
|
||||
return contextPaintIsEqual && mViewportSize == aOther.mViewportSize &&
|
||||
mPreserveAspectRatio == aOther.mPreserveAspectRatio &&
|
||||
mColorScheme == aOther.mColorScheme &&
|
||||
@@ -57,17 +57,17 @@ index b1f811989697966e30859610f4fc07579d0385ab..f6f4cd5a45fc49b416111d980fab4e61
|
||||
}
|
||||
|
||||
bool operator!=(const SVGImageContext&) const = default;
|
||||
@@ -129,7 +154,8 @@ class SVGImageContext {
|
||||
return HashGeneric(hash, mViewportSize.map(HashSize).valueOr(0),
|
||||
mPreserveAspectRatio.map(HashPAR).valueOr(0),
|
||||
mColorScheme.map(HashColorScheme).valueOr(0),
|
||||
- HashLinkParameters(mLinkParameters));
|
||||
+ HashLinkParameters(mLinkParameters), mZenBoostsAccent,
|
||||
+ mZenBoostsComplementaryRotation, mZenBoostsInverted);
|
||||
@@ -123,7 +148,8 @@ class SVGImageContext {
|
||||
return AddToHash(hash, mViewportSize.map(HashSize).valueOr(0),
|
||||
mPreserveAspectRatio.map(HashPAR).valueOr(0),
|
||||
mColorScheme.map(HashColorScheme).valueOr(0),
|
||||
- HashLinkParameters(mLinkParameters));
|
||||
+ HashLinkParameters(mLinkParameters), mZenBoostsAccent,
|
||||
+ mZenBoostsComplementaryRotation, mZenBoostsInverted);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -167,6 +193,9 @@ class SVGImageContext {
|
||||
@@ -156,6 +182,9 @@ class SVGImageContext {
|
||||
Maybe<SVGPreserveAspectRatio> mPreserveAspectRatio;
|
||||
Maybe<ColorScheme> mColorScheme;
|
||||
StyleLinkParameters mLinkParameters;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
|
||||
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
|
||||
index 4f4efe572909441abdd81909282936702c225621..2b47a2c9e97cf87641a0be9814f9a6be4d43f6ea 100644
|
||||
--- a/services/sync/modules/service.sys.mjs
|
||||
+++ b/services/sync/modules/service.sys.mjs
|
||||
@@ -99,6 +99,10 @@ function getEngineModules() {
|
||||
@@ -7,8 +7,8 @@ index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f0
|
||||
whenFalse: "ExtensionStorageEngineBridge",
|
||||
};
|
||||
+ result.Spaces = {
|
||||
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
|
||||
+ symbol: "ZenWorkspacesEngine",
|
||||
+ module: "resource:///modules/zen/ZenSpacesSync.sys.mjs",
|
||||
+ symbol: "ZenSpacesSyncEngine",
|
||||
+ };
|
||||
return result;
|
||||
}
|
||||
|
||||
14
src/testing/xpcshell/runxpcshelltests-py.patch
Normal file
14
src/testing/xpcshell/runxpcshelltests-py.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py
|
||||
index 403401613189ffb758037850dc6bbb1d55f194ab..cad28e245155eb54775e551885532026e56c5fa9 100755
|
||||
--- a/testing/xpcshell/runxpcshelltests.py
|
||||
+++ b/testing/xpcshell/runxpcshelltests.py
|
||||
@@ -2275,6 +2275,9 @@ class XPCShellTests:
|
||||
appDirKey = None
|
||||
if "appname" in self.mozInfo:
|
||||
appDirKey = self.mozInfo["appname"] + "-appdir"
|
||||
+ if appDirKey == "zen-appdir":
|
||||
+ # Inherited manifests use firefox-appdir.
|
||||
+ appDirKey = "firefox-appdir"
|
||||
|
||||
# We have to do this before we run tests that depend on having the node
|
||||
# http/2 server.
|
||||
@@ -1,37 +0,0 @@
|
||||
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
||||
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
|
||||
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
||||
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
||||
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
- create(name, icon, color) {
|
||||
+ create(name, icon, color, id = null) {
|
||||
this.ensureDataReady();
|
||||
|
||||
- // Retrieve the next userContextId available.
|
||||
- let userContextId = ++this._lastUserContextId;
|
||||
+ if (id !== null) {
|
||||
+ id = typeof id == "string" ? Number(id) : id;
|
||||
+ if (!Number.isSafeInteger(id) || id <= 0) {
|
||||
+ throw new Error(`Invalid userContextId '${id}'`);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ let userContextId;
|
||||
+ if (
|
||||
+ id !== null &&
|
||||
+ !this._identities.some(identity => identity.userContextId === id)
|
||||
+ ) {
|
||||
+ userContextId = id;
|
||||
+ this._lastUserContextId = Math.max(
|
||||
+ this._lastUserContextId,
|
||||
+ userContextId
|
||||
+ );
|
||||
+ } else {
|
||||
+ userContextId = ++this._lastUserContextId;
|
||||
+ }
|
||||
|
||||
// Throw an error if the next userContextId available is invalid (the one associated to
|
||||
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.tokens.css b/toolkit/content/widgets/moz-toggle/moz-toggle.tokens.css
|
||||
index 6247d7a3923a6fedd4ec9911aba309ce565cd498..744e670eccc936dde664aa07df3e87bdcafd5e48 100644
|
||||
index c0976c54418a28046802443931042bd285aefbde..2adb18d25edbb8aac4e396fcfc64a4a664607836 100644
|
||||
--- a/toolkit/content/widgets/moz-toggle/moz-toggle.tokens.css
|
||||
+++ b/toolkit/content/widgets/moz-toggle/moz-toggle.tokens.css
|
||||
@@ -13,8 +13,8 @@
|
||||
@@ -11,5 +11,5 @@ index 6247d7a3923a6fedd4ec9911aba309ce565cd498..744e670eccc936dde664aa07df3e87bd
|
||||
+ --toggle-background-color-pressed: var(--zen-primary-color);
|
||||
+ --toggle-background-color-pressed-hover: var(--zen-colors-hover-bg);
|
||||
--toggle-background-color-pressed-active: var(--color-accent-primary-active);
|
||||
--toggle-border-color: var(--border-color-interactive);
|
||||
--toggle-border-color: light-dark(var(--color-gray-70), var(--border-color-interactive));
|
||||
--toggle-border-color-hover: var(--toggle-border-color);
|
||||
|
||||
29
src/toolkit/content/widgets/panel-list/panel-list-mjs.patch
Normal file
29
src/toolkit/content/widgets/panel-list/panel-list-mjs.patch
Normal file
@@ -0,0 +1,29 @@
|
||||
diff --git a/toolkit/content/widgets/panel-list/panel-list.mjs b/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||
index 68ad2fafc8b31dfff541cf8381d9b9533e376d1c..fa62fec6bfb33338091fc06d3736fb76fd5c47c4 100644
|
||||
--- a/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||
+++ b/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||
@@ -223,7 +223,12 @@ export class PanelList extends HTMLElement {
|
||||
this.setAttribute("showing", "true");
|
||||
// Tell the host element to hide any overflow in case the panel extends off
|
||||
// the page before the alignment is set.
|
||||
- hostElement.style.overflow = "hidden";
|
||||
+ const hostHidesOverflow = !getComputedStyle(hostElement)
|
||||
+ .overflow.split(" ")
|
||||
+ .includes("visible");
|
||||
+ if (!hostHidesOverflow) {
|
||||
+ hostElement.style.overflow = "hidden";
|
||||
+ }
|
||||
|
||||
// Wait for a layout flush, then find the bounds.
|
||||
let {
|
||||
@@ -344,7 +349,9 @@ export class PanelList extends HTMLElement {
|
||||
// Set the alignments and show the panel.
|
||||
this.setAttribute("align", align);
|
||||
this.setAttribute("valign", valign);
|
||||
- hostElement.style.overflow = "";
|
||||
+ if (!hostHidesOverflow) {
|
||||
+ hostElement.style.overflow = "";
|
||||
+ }
|
||||
// Decide positioning based on where this panel will be rendered
|
||||
const offsetParentIsBody =
|
||||
this.supportsPopover() ||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
|
||||
index 312011c1c2ac41e78affecbdef25d72766a7c882..e5865c7d6259f0a3df48edf7a26b1b75a23cdb3a 100644
|
||||
index 22a2a7ef3a5e9257c8bb00ba6a55e8d39d2abbd9..6d8353fd11921585f7101f5717bdb3dd8f2a5eae 100644
|
||||
--- a/widget/cocoa/nsCocoaWindow.h
|
||||
+++ b/widget/cocoa/nsCocoaWindow.h
|
||||
@@ -462,6 +462,10 @@ class nsCocoaWindow final : public nsIWidget {
|
||||
@@ -461,6 +461,10 @@ class nsCocoaWindow final : public nsIWidget {
|
||||
void SetHideTitlebarSeparator(bool) override;
|
||||
bool IsMacTitlebarDirectionRTL() override;
|
||||
void SetCustomTitlebar(bool) override;
|
||||
@@ -13,9 +13,9 @@ index 312011c1c2ac41e78affecbdef25d72766a7c882..e5865c7d6259f0a3df48edf7a26b1b75
|
||||
void UpdateThemeGeometries(
|
||||
const nsTArray<ThemeGeometry>& aThemeGeometries) override;
|
||||
void LockAspectRatio(bool aShouldLock) override;
|
||||
@@ -672,6 +676,7 @@ class nsCocoaWindow final : public nsIWidget {
|
||||
bool mAspectRatioLocked = false;
|
||||
bool mIsAlert = false; // True if this is an non-native alert window.
|
||||
@@ -674,6 +678,7 @@ class nsCocoaWindow final : public nsIWidget {
|
||||
// show (e.g. a window created by detaching a tab from a fullscreen window).
|
||||
bool mIsInitialFullscreenSuppressed = false;
|
||||
bool mWasShown = false;
|
||||
+ bool mIsBrowserWindow = false;
|
||||
|
||||
|
||||
29
src/widget/gtk/nsDragSessionSource-cpp.patch
Normal file
29
src/widget/gtk/nsDragSessionSource-cpp.patch
Normal file
@@ -0,0 +1,29 @@
|
||||
diff --git a/widget/gtk/nsDragSessionSource.cpp b/widget/gtk/nsDragSessionSource.cpp
|
||||
index a5f67d700f8c80d513f08cc25405b57cf28a2a9a..58186470bcbb7b1eda34ee20a19e29d540ab052a 100644
|
||||
--- a/widget/gtk/nsDragSessionSource.cpp
|
||||
+++ b/widget/gtk/nsDragSessionSource.cpp
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/StaticPrefs_widget.h"
|
||||
#include "mozilla/WidgetUtilsGtk.h"
|
||||
+#include "mozilla/nsZenDragAndDrop.h"
|
||||
#include "nsAppShell.h"
|
||||
#include "nsArrayUtils.h"
|
||||
#include "nsCRT.h"
|
||||
@@ -1196,11 +1197,15 @@ bool nsDragSessionSource::SetAlphaPixmap(SourceSurface* aSurface,
|
||||
cairo_image_surface_get_stride(surf), SurfaceFormat::B8G8R8A8);
|
||||
if (!dt) return false;
|
||||
|
||||
+ auto drag_translucency = DRAG_IMAGE_ALPHA_LEVEL;
|
||||
+ if (auto zenDragAndDrop = zen::nsZenDragAndDrop::GetZenDragAndDropInstance()) {
|
||||
+ drag_translucency = zenDragAndDrop->GetDragImageOpacity();
|
||||
+ }
|
||||
dt->ClearRect(Rect(0, 0, dragRect.width, dragRect.height));
|
||||
dt->DrawSurface(
|
||||
aSurface, Rect(0, 0, dragRect.width, dragRect.height),
|
||||
Rect(0, 0, dragRect.width, dragRect.height), DrawSurfaceOptions(),
|
||||
- DrawOptions(DRAG_IMAGE_ALPHA_LEVEL, CompositionOp::OP_SOURCE));
|
||||
+ DrawOptions(drag_translucency, CompositionOp::OP_SOURCE));
|
||||
|
||||
cairo_surface_mark_dirty(surf);
|
||||
cairo_surface_set_device_offset(surf, -aXOffset, -aYOffset);
|
||||
@@ -49,9 +49,9 @@
|
||||
<vbox flex="1" id="zen-boost-filter-wrapper">
|
||||
<hbox class="zen-boost-color-picker-gradient zen-boost-panel-disabled">
|
||||
<button data-l10n-id="zen-boost-magic-theme" id="zen-boost-magic-theme" class="subviewbutton mod-button"></button>
|
||||
<html:div id="zen-boost-color-picker-dot-primary" class="zen-boost-color-picker-dot"></html:div>
|
||||
<html:div id="zen-boost-color-picker-dot-secondary" class="zen-boost-color-picker-dot"></html:div>
|
||||
<html:div class="zen-boost-color-picker-circle"></html:div>
|
||||
<html:div id="zen-boost-color-picker-dot-primary" class="zen-boost-color-picker-dot no-squircles"></html:div>
|
||||
<html:div id="zen-boost-color-picker-dot-secondary" class="zen-boost-color-picker-dot no-squircles"></html:div>
|
||||
<html:div class="zen-boost-color-picker-circle no-squircles"></html:div>
|
||||
</hbox>
|
||||
|
||||
<hbox flex="1" id="zen-boost-toolbar-wrapper-colors">
|
||||
|
||||
@@ -194,7 +194,8 @@ body {
|
||||
max-height: 40px;
|
||||
align-items: center;
|
||||
background-color: var(--zen-boosts-secondary-background);
|
||||
border: solid 1px var(--zen-boosts-primary-border-color);
|
||||
border: solid 1px transparent;
|
||||
border-bottom-color: var(--zen-boosts-primary-border-color);
|
||||
|
||||
@media (-moz-windows-mica) {
|
||||
background-color: var(--zen-boosts-mica-background);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import checkForZenUpdates, {
|
||||
createWindowUpdateAnimation,
|
||||
playWindowSweepAnimation,
|
||||
} from "chrome://browser/content/ZenUpdates.mjs";
|
||||
|
||||
class ZenStartup {
|
||||
@@ -195,6 +196,14 @@ class ZenStartup {
|
||||
checkForZenUpdates();
|
||||
return await createWindowUpdateAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for privileged modules (e.g. the sync applier) to play the
|
||||
* update sweep animation in this window.
|
||||
*/
|
||||
playWindowSweepAnimation() {
|
||||
return playWindowSweepAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
window.gZenStartup = new ZenStartup();
|
||||
|
||||
@@ -493,6 +493,13 @@ window.gZenUIManager = {
|
||||
_clearTimeout: null,
|
||||
_lastTab: null,
|
||||
|
||||
_urlbarSessionCounter: 0,
|
||||
_urlbarOwner: null,
|
||||
|
||||
_isOwner(closeSeq) {
|
||||
return this._urlbarOwner === closeSeq;
|
||||
},
|
||||
|
||||
// Check if browser elements are in a valid state for tab operations
|
||||
_validateBrowserState() {
|
||||
// Check if browser window is still open
|
||||
@@ -517,8 +524,15 @@ window.gZenUIManager = {
|
||||
werePassedURL,
|
||||
searchClipboard,
|
||||
where,
|
||||
overridePreferance = false
|
||||
overridePreferance = false,
|
||||
closeToken = {}
|
||||
) {
|
||||
// Keep this here. It's better to make sure every
|
||||
// call has a different token rather then forgetting
|
||||
// to increment it in one of the early returns.
|
||||
const closeSeq = ++this._urlbarSessionCounter;
|
||||
closeToken.id = closeSeq;
|
||||
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn("Browser state invalid for new tab operation");
|
||||
@@ -542,7 +556,13 @@ window.gZenUIManager = {
|
||||
|
||||
// Close the new tab popup on cmd/ctrl + t
|
||||
if (!overridePreferance && gURLBar.hasAttribute("zen-newtab")) {
|
||||
this.handleUrlbarClose();
|
||||
// The _zenHandleUrlbarClose holds a token in it's closure.
|
||||
// Call that instead.
|
||||
if (gURLBar._zenHandleUrlbarClose) {
|
||||
gURLBar._zenHandleUrlbarClose();
|
||||
} else {
|
||||
this.handleUrlbarClose(closeSeq);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -568,7 +588,9 @@ window.gZenUIManager = {
|
||||
this._prevUrlbarLabel = gURLBar._untrimmedValue || "";
|
||||
|
||||
// Set up URL bar for new tab
|
||||
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
|
||||
gURLBar._zenHandleUrlbarClose = (onSwitch, onElementPicked) =>
|
||||
this.handleUrlbarClose(closeSeq, onSwitch, onElementPicked);
|
||||
this._urlbarOwner = closeSeq;
|
||||
gURLBar.setAttribute("zen-newtab", true);
|
||||
|
||||
// Update newtab buttons
|
||||
@@ -582,18 +604,33 @@ window.gZenUIManager = {
|
||||
document.getElementById("Browser:OpenLocation").doCommand();
|
||||
} catch (e) {
|
||||
console.error("Error opening location in new tab:", e);
|
||||
this.handleUrlbarClose(false);
|
||||
this.handleUrlbarClose(closeSeq, false, false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether the closeToken given by handleNewTab
|
||||
* corresponds to a close event.
|
||||
*
|
||||
* @param {object} closeToken - Token previously passed to handleNewTab.
|
||||
* @param {CustomEvent} event - A ZenURLBarClosed event.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
matchesCloseToken(closeToken, event) {
|
||||
return (
|
||||
typeof closeToken?.id === "number" &&
|
||||
event?.detail?.closeSeq === closeToken.id
|
||||
);
|
||||
},
|
||||
|
||||
clearUrlbarData() {
|
||||
this._prevUrlbarLabel = null;
|
||||
this._lastSearch = "";
|
||||
},
|
||||
|
||||
handleUrlbarClose(onSwitch = false, onElementPicked = false) {
|
||||
handleUrlbarClose(closeSeq, onSwitch = false, onElementPicked = false) {
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn("Browser state invalid for URL bar close operation");
|
||||
@@ -607,59 +644,69 @@ window.gZenUIManager = {
|
||||
|
||||
const isFocusedBefore = gURLBar.focused;
|
||||
setTimeout(() => {
|
||||
// We use this attribute on Tabbrowser::addTab
|
||||
gURLBar.removeAttribute("zen-newtab");
|
||||
/* If someone else opened the url bar in the meantime don't be
|
||||
* stingy and leave it open for them.*/
|
||||
if (this._isOwner(closeSeq)) {
|
||||
// We use this attribute on Tabbrowser::addTab
|
||||
gURLBar.removeAttribute("zen-newtab");
|
||||
|
||||
// Safely restore tab visual state with proper validation
|
||||
if (
|
||||
this._lastTab &&
|
||||
!this._lastTab.closing &&
|
||||
this._lastTab.documentGlobal &&
|
||||
!this._lastTab.documentGlobal.closed &&
|
||||
gBrowser.selectedTab === this._lastTab
|
||||
) {
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
}
|
||||
|
||||
// Reset newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute("in-urlbar");
|
||||
}
|
||||
|
||||
// Handle search data
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue || "";
|
||||
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
// Safely restore tab visual state with proper validation
|
||||
if (
|
||||
this._lastTab &&
|
||||
!this._lastTab.closing &&
|
||||
this._lastTab.documentGlobal &&
|
||||
!this._lastTab.documentGlobal.closed &&
|
||||
gBrowser.selectedTab === this._lastTab
|
||||
) {
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
}
|
||||
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
// Reset newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute("in-urlbar");
|
||||
}
|
||||
|
||||
// Handle search data
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue || "";
|
||||
|
||||
// Safely restore URL bar state with proper validation
|
||||
if (this._prevUrlbarLabel) {
|
||||
gURLBar.setURI({
|
||||
uri: this._prevUrlbarLabel,
|
||||
dueToTabSwitch: onSwitch,
|
||||
isSameDocument: !onSwitch,
|
||||
});
|
||||
}
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
}
|
||||
|
||||
gURLBar.handleRevert();
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
|
||||
// Safely restore URL bar state with proper validation
|
||||
if (this._prevUrlbarLabel) {
|
||||
gURLBar.setURI({
|
||||
uri: this._prevUrlbarLabel,
|
||||
dueToTabSwitch: onSwitch,
|
||||
isSameDocument: !onSwitch,
|
||||
});
|
||||
}
|
||||
|
||||
gURLBar.handleRevert();
|
||||
}
|
||||
|
||||
if (isFocusedBefore) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("ZenURLBarClosed", {
|
||||
detail: { onSwitch, onElementPicked },
|
||||
detail: { onSwitch, onElementPicked, closeSeq },
|
||||
})
|
||||
);
|
||||
|
||||
if (!this._isOwner(closeSeq)) {
|
||||
return;
|
||||
}
|
||||
this._urlbarOwner = null;
|
||||
|
||||
gURLBar.view.close({ elementPicked: onElementPicked });
|
||||
gURLBar.updateTextOverflow();
|
||||
|
||||
@@ -683,6 +730,8 @@ window.gZenUIManager = {
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (this._isOwner(closeSeq)) {
|
||||
this._urlbarOwner = null;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
@@ -1233,7 +1282,7 @@ window.gZenVerticalTabsManager = {
|
||||
if (!this._hasSetSingleToolbar) {
|
||||
height = AppConstants.platform == "macosx" ? 34 : 32;
|
||||
} else if (!gURLBar.hasAttribute("breakout-extend")) {
|
||||
height = 38;
|
||||
height = 36;
|
||||
}
|
||||
if (typeof height !== "undefined") {
|
||||
gURLBar.style.setProperty("--urlbar-height", `${height}px`);
|
||||
@@ -1299,18 +1348,21 @@ window.gZenVerticalTabsManager = {
|
||||
document.documentElement.removeAttribute("zen-right-side");
|
||||
}
|
||||
|
||||
delete this._hadSidebarCollapse;
|
||||
if (isSidebarExpanded) {
|
||||
this._hadSidebarCollapse = !document.documentElement.hasAttribute(
|
||||
"zen-sidebar-expanded"
|
||||
);
|
||||
this.navigatorToolbox.setAttribute("zen-sidebar-expanded", "true");
|
||||
document.documentElement.setAttribute("zen-sidebar-expanded", "true");
|
||||
gBrowser.tabContainer.setAttribute("expanded", "true");
|
||||
} else {
|
||||
this.navigatorToolbox.removeAttribute("zen-sidebar-expanded");
|
||||
document.documentElement.removeAttribute("zen-sidebar-expanded");
|
||||
gBrowser.tabContainer.removeAttribute("expanded");
|
||||
const wasSidebarExpanded = document.documentElement.hasAttribute(
|
||||
"zen-sidebar-expanded"
|
||||
);
|
||||
if (isSidebarExpanded !== wasSidebarExpanded) {
|
||||
delete this._hadSidebarCollapse;
|
||||
if (isSidebarExpanded) {
|
||||
this._hadSidebarCollapse = true;
|
||||
this.navigatorToolbox.setAttribute("zen-sidebar-expanded", "true");
|
||||
document.documentElement.setAttribute("zen-sidebar-expanded", "true");
|
||||
gBrowser.tabContainer.setAttribute("expanded", "true");
|
||||
} else {
|
||||
this.navigatorToolbox.removeAttribute("zen-sidebar-expanded");
|
||||
document.documentElement.removeAttribute("zen-sidebar-expanded");
|
||||
gBrowser.tabContainer.removeAttribute("expanded");
|
||||
}
|
||||
}
|
||||
|
||||
const appContentNavbarContaienr = document.getElementById(
|
||||
|
||||
@@ -61,6 +61,14 @@ export async function createWindowUpdateAnimation() {
|
||||
return;
|
||||
}
|
||||
Services.prefs.setStringPref(ZEN_BUILD_ID_PREF, appID);
|
||||
await playWindowSweepAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the full-window sweep shown after updates. Also used the first
|
||||
* time incoming sync data is applied on this profile.
|
||||
*/
|
||||
export async function playWindowSweepAnimation() {
|
||||
await gZenWorkspaces.promiseInitialized;
|
||||
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||
const element = document.createElement("div");
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
--urlbarview-separator-color: light-dark(hsl(0, 0%, 80%), hsl(0, 0%, 20%));
|
||||
--urlbarview-background-color-hover: var(--toolbarbutton-hover-background);
|
||||
border-radius: calc(var(--toolbarbutton-border-radius) - 2px);
|
||||
height: var(--urlbar-height);
|
||||
--urlbarView-results-padding: 10px !important;
|
||||
--urlbar-container-height: 48px !important;
|
||||
--urlbar-input-container-border: none;
|
||||
|
||||
--urlbar-margin-inline: 5px;
|
||||
--urlbar-container-padding: 4px;
|
||||
@@ -326,6 +326,7 @@
|
||||
order: 2;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
#notification-popup-box:not([open]):not(:has(> [showing="true"])) {
|
||||
margin-inline-start: calc(-10px - 2 * var(--urlbar-icon-padding));
|
||||
opacity: 0;
|
||||
@@ -487,6 +488,7 @@
|
||||
translate: -50% 0%;
|
||||
left: 50% !important;
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
#urlbar-container:has(&) {
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--toolbarbutton-hover-background);
|
||||
@@ -633,6 +635,7 @@
|
||||
|
||||
#urlbar-label-box,
|
||||
#urlbar-search-mode-indicator {
|
||||
--urlbar-basic-component-size: 18px;
|
||||
background-color: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.2)) 50%) !important;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
@@ -717,6 +720,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.urlbarView {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#urlbar-search-mode-indicator-title {
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
|
||||
@@ -335,7 +335,7 @@ menuseparator {
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
padding: var(--zen-toast-padding);
|
||||
border-radius: 10px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(to bottom, var(--zen-primary-color) -40%, color-mix(in srgb, var(--zen-primary-color), #0f0f0f 20%));
|
||||
box-shadow: light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.4)) 0 0 22px 2px;
|
||||
border: none;
|
||||
@@ -374,7 +374,7 @@ menuseparator {
|
||||
min-width: unset !important;
|
||||
min-height: 28px;
|
||||
margin: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
border-radius: 10px !important;
|
||||
border-top: 2px solid light-dark(transparent, rgba(255, 255, 255, 0.1));
|
||||
color: light-dark(rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0.8)) !important;
|
||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
|
||||
@@ -78,13 +78,13 @@
|
||||
}
|
||||
|
||||
.zen-sidebar-notification-progress {
|
||||
height: 2px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
transform: translateY(-1px);
|
||||
|
||||
& .zen-sidebar-notification-progress-bar {
|
||||
height: 100%;
|
||||
background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
||||
background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, transparent 10%, #6b6b6b));
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
.toolbarbutton-1:not(#tabs-newtab-button),
|
||||
.urlbar-page-action,
|
||||
.identity-box-button {
|
||||
--tab-border-radius: 6px;
|
||||
--tab-border-radius: 8px;
|
||||
--toolbarbutton-border-radius: var(--tab-border-radius);
|
||||
--toolbarbutton-padding-inner: 6px;
|
||||
--toolbarbutton-padding-outer: 1px;
|
||||
@@ -502,11 +502,13 @@
|
||||
opacity 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
.permission-popup-permission-item:hover &::before,
|
||||
.permission-popup-boost-item:hover:not(:has(.zen-permission-popup-boost-editor-button:hover)) &::before {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
.permission-popup-permission-item:active:hover &::before,
|
||||
.permission-popup-boost-item:active:hover:not(:has(.zen-permission-popup-boost-editor-button:hover)) &::before {
|
||||
transform: scale(0.95);
|
||||
@@ -601,6 +603,7 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
background: linear-gradient(to bottom, light-dark(rgb(255, 255, 255), rgb(34, 34, 34)), light-dark(rgb(246, 246, 246), rgb(21, 21, 21)));
|
||||
|
||||
|
||||
@@ -92,7 +92,11 @@
|
||||
--in-content-accent-color-active: currentColor !important;
|
||||
|
||||
/* This is like the secondary button */
|
||||
--in-content-button-background: transparent !important;
|
||||
--in-content-button-background: color-mix(
|
||||
in srgb,
|
||||
currentColor,
|
||||
transparent 90%
|
||||
) !important;
|
||||
--in-content-button-text-color: var(--zen-secondary-btn-color) !important;
|
||||
--in-content-button-background-hover: color-mix(
|
||||
in srgb,
|
||||
@@ -213,6 +217,16 @@
|
||||
--toolbarbutton-border-radius: 6px;
|
||||
--urlbar-margin-inline: 1px !important;
|
||||
|
||||
--zen-squircle-value: 1.3;
|
||||
|
||||
@media (-moz-platform: windows) {
|
||||
--zen-squircle-value: 2.1;
|
||||
}
|
||||
|
||||
@media (-moz-platform: linux) {
|
||||
--zen-squircle-value: 1.2;
|
||||
}
|
||||
|
||||
--tab-icon-overlay-stroke: light-dark(white, black) !important;
|
||||
--tab-close-button-padding: 4px !important;
|
||||
|
||||
@@ -344,3 +358,11 @@
|
||||
}
|
||||
|
||||
%include zen-buttons.css
|
||||
|
||||
@media (-moz-pref('layout.css.corner-shape.enabled')) {
|
||||
*:not(.no-squircles),
|
||||
.zen-squircle-before::before,
|
||||
.zen-squircle-after::after {
|
||||
corner-shape: superellipse(var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,8 +832,9 @@ window.gZenCompactModeManager = {
|
||||
|
||||
const onLeave = event => {
|
||||
if (AppConstants.platform == "macosx") {
|
||||
const buttonRect =
|
||||
gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
|
||||
const buttonRect = window.windowUtils.getBoundsWithoutFlushing(
|
||||
gZenVerticalTabsManager.actualWindowButtons
|
||||
);
|
||||
const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
|
||||
const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
|
||||
if (
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
&,
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: calc(var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px));
|
||||
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)) * var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,10 +183,10 @@
|
||||
if (tabClone.hasAttribute("visuallyselected")) {
|
||||
tabClone.style.transform = "translate(-50%, -50%)";
|
||||
}
|
||||
} else if (AppConstants.platform !== "macosx" && !tab.isZenFolder) {
|
||||
// On windows and linux, we still don't add some extra opaqueness
|
||||
// for the tab to be more visible. This is a hacky workaround.
|
||||
// TODO: Make windows and linux DnD use nsZenDragAndDrop::mDragImageOpacity
|
||||
} else if (AppConstants.platform == "win" && !tab.isZenFolder) {
|
||||
// On windows, the shell applies its own translucency to the drag
|
||||
// image which we can't control, so force some extra contrast for
|
||||
// the tab to be more visible. This is a hacky workaround.
|
||||
tabClone.style.colorScheme = "light";
|
||||
tabClone.style.setProperty("--tab-selected-textcolor", "black");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class nsZenFolder extends MozTabbrowserTabGroup {
|
||||
#initialized = false;
|
||||
|
||||
static markup = `
|
||||
<hbox class="tab-group-label-container zen-drop-target" pack="center">
|
||||
<hbox class="tab-group-label-container zen-squircle-before zen-drop-target" pack="center">
|
||||
<html:div class="tab-group-folder-icon"/>
|
||||
<label class="tab-group-label" role="button"/>
|
||||
<image class="tab-reset-button reset-icon" role="button" keyNav="false" data-l10n-id="zen-folders-unload-all-tooltip"/>
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
|
||||
&:hover {
|
||||
background: light-dark(rgb(41, 41, 41), rgb(204, 204, 204));
|
||||
scale: 1.05;
|
||||
scale: 1.02;
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
scale: 0.95;
|
||||
scale: 0.98;
|
||||
}
|
||||
|
||||
& label {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<html:template id="zen-glance-sidebar-template">
|
||||
<vbox class="zen-glance-sidebar-container">
|
||||
<toolbarbutton class="zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
|
||||
<toolbarbutton class="zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
|
||||
<toolbarbutton class="zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
|
||||
<toolbarbutton class="no-squircles zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
|
||||
<toolbarbutton class="no-squircles zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
|
||||
<toolbarbutton class="no-squircles zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
|
||||
</vbox>
|
||||
</html:template>
|
||||
|
||||
@@ -200,6 +200,50 @@ class nsZenLiveFoldersManager {
|
||||
return this.liveFolders.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider config needed to recreate this live folder on
|
||||
* another device, or null for non-live folders. Fetch bookkeeping fields
|
||||
* are stripped since they are device-local.
|
||||
*
|
||||
* @param {string} id - The folder ID.
|
||||
*/
|
||||
getSyncableFolderData(id) {
|
||||
const liveFolder = this.liveFolders.get(id);
|
||||
if (!liveFolder) {
|
||||
return null;
|
||||
}
|
||||
const state = { ...liveFolder.serialize().state };
|
||||
delete state.lastFetched;
|
||||
delete state.lastErrorId;
|
||||
return { type: liveFolder.constructor.type, state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a live folder provider for a folder that arrived from
|
||||
* Firefox Sync. The folder element must already exist.
|
||||
*
|
||||
* @param {string} id - The folder ID.
|
||||
* @param {{ type: string, state: object }} config - Synced provider config.
|
||||
*/
|
||||
async adoptSyncedFolder(id, { type, state } = {}) {
|
||||
await this.stateRestored.promise;
|
||||
if (this.liveFolders.has(id)) {
|
||||
return;
|
||||
}
|
||||
const ProviderClass = this.registry.get(type);
|
||||
if (!ProviderClass || !state) {
|
||||
return;
|
||||
}
|
||||
const liveFolder = new ProviderClass({
|
||||
id,
|
||||
state: this.#applyDefaultStateValues({ ...state }),
|
||||
manager: this,
|
||||
});
|
||||
this.liveFolders.set(id, liveFolder);
|
||||
liveFolder.start();
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
async createFolder(type) {
|
||||
const [provider, providerType] = type.split(":");
|
||||
let ProviderClass = this.registry.get(provider);
|
||||
@@ -324,7 +368,7 @@ class nsZenLiveFoldersManager {
|
||||
},
|
||||
],
|
||||
content: {
|
||||
width: "310px",
|
||||
width: "312px",
|
||||
position: "callout",
|
||||
title_logo: {
|
||||
imageURL: icon,
|
||||
@@ -619,6 +663,7 @@ class nsZenLiveFoldersManager {
|
||||
async #restoreState() {
|
||||
let data = await this.#readStateFromDisk();
|
||||
if (!Array.isArray(data)) {
|
||||
this.stateRestored.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ class ZenMediaCard {
|
||||
|
||||
onToggle() {
|
||||
if (this.controller?.isPlaying) {
|
||||
this.controller.pause();
|
||||
this.controller.pause("user");
|
||||
} else {
|
||||
this.controller?.play();
|
||||
}
|
||||
@@ -365,7 +365,7 @@ class ZenMediaCard {
|
||||
}
|
||||
|
||||
onSeekDrag(event) {
|
||||
this.controller?.pause();
|
||||
this.controller?.pause("user");
|
||||
const newTime = (event.target.value / 100) * this.duration;
|
||||
this.currentTimeEl.textContent = this.formatSecondsToTime(newTime);
|
||||
}
|
||||
@@ -408,7 +408,7 @@ class ZenMediaCard {
|
||||
|
||||
onClose() {
|
||||
if (this.controller) {
|
||||
this.controller.pause();
|
||||
this.controller.pause("user");
|
||||
} else {
|
||||
const webRTC =
|
||||
this.browser.browsingContext.currentWindowGlobal.getActor("WebRTC");
|
||||
|
||||
@@ -10,7 +10,6 @@ const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenLiveFoldersManager:
|
||||
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||
@@ -611,7 +610,9 @@ export class nsZenSessionManager {
|
||||
}
|
||||
);
|
||||
this.#collectWindowData(windows);
|
||||
lazy.ZenSyncStore.notifyAboutChanges();
|
||||
// Let interested consumers (e.g. Firefox Sync) know fresh sidebar data
|
||||
// is available, without this module knowing anything about them.
|
||||
Services.obs.notifyObservers(null, "zen-sidebar-data-collected");
|
||||
// This would save the data to disk asynchronously or when quitting the app.
|
||||
let sidebar = this.#sidebarWithoutCloning;
|
||||
this.#file.data = sidebar;
|
||||
|
||||
@@ -606,7 +606,9 @@ class nsZenWindowSync {
|
||||
if (isTab) {
|
||||
if (originalIsEssential !== targetIsEssential) {
|
||||
if (originalIsEssential) {
|
||||
gZenPinnedTabManager.addToEssentials(aTargetItem);
|
||||
gZenPinnedTabManager.addToEssentials(aTargetItem, {
|
||||
replicating: true,
|
||||
});
|
||||
} else {
|
||||
gZenPinnedTabManager.removeEssentials(
|
||||
aTargetItem,
|
||||
@@ -1232,7 +1234,7 @@ class nsZenWindowSync {
|
||||
activeIndex = Math.min(activeIndex, entries.length - 1);
|
||||
activeIndex = Math.max(activeIndex, 0);
|
||||
let entryToUse = (entries[activeIndex] || entries[0]) ?? null;
|
||||
this.#setPinnedInitialState(
|
||||
this.setPinnedInitialState(
|
||||
aTab,
|
||||
{ url: entryToUse?.url, title: entryToUse?.title },
|
||||
image
|
||||
@@ -1250,14 +1252,22 @@ class nsZenWindowSync {
|
||||
*/
|
||||
setPinnedUrl(aTab, aUrl, aImage) {
|
||||
this.log(`Setting pinned url for tab ${aTab.id}`);
|
||||
this.#setPinnedInitialState(
|
||||
this.setPinnedInitialState(
|
||||
aTab,
|
||||
{ url: aUrl, title: aTab.zenStaticLabel },
|
||||
aImage
|
||||
);
|
||||
}
|
||||
|
||||
#setPinnedInitialState(aTab, aEntry, aImage) {
|
||||
/**
|
||||
* Sets the pinned initial state (canonical entry and icon) for a tab's
|
||||
* instances across all windows.
|
||||
*
|
||||
* @param {object} aTab - Any window's instance of the tab.
|
||||
* @param {object} aEntry - The canonical { url, title } entry.
|
||||
* @param {string} [aImage] - Optional icon to store.
|
||||
*/
|
||||
setPinnedInitialState(aTab, aEntry, aImage) {
|
||||
const initialState = { entry: aEntry, image: aImage };
|
||||
this.#runOnAllWindows(null, win => {
|
||||
const targetTab = this.getItemFromWindow(win, aTab.id);
|
||||
@@ -1362,7 +1372,9 @@ class nsZenWindowSync {
|
||||
return;
|
||||
}
|
||||
tab._zenContentsVisible = true;
|
||||
tab.id = this.#newTabSyncId;
|
||||
if (!tab.id) {
|
||||
tab.id = this.#newTabSyncId;
|
||||
}
|
||||
if (lazy.gSyncOnlyPinnedTabs && !tab.pinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class nsZenSpaceRoutingManager {
|
||||
const element = window.MozXULElement.parseXULToFragment(`
|
||||
<menuseparator/>
|
||||
<menuitem id="context_zen-add-domain-to-routing"
|
||||
data-lazy-l10n-id="tab-context-zen-add-domain-to-sr"
|
||||
data-l10n-id="tab-context-zen-add-domain-to-sr"
|
||||
data-l10n-args='{"tabCount": 1}'/>
|
||||
`);
|
||||
window.document.getElementById("context_undoCloseTab").after(element);
|
||||
|
||||
@@ -52,7 +52,7 @@ export class nsZenWorkspace extends MozXULElement {
|
||||
|
||||
static get markup() {
|
||||
return `
|
||||
<vbox class="zen-workspace-tabs-section zen-current-workspace-indicator zen-drop-target" flex="1" context="zenWorkspaceMoreActions">
|
||||
<vbox class="zen-workspace-tabs-section zen-current-workspace-indicator zen-squircle-before zen-drop-target" flex="1" context="zenWorkspaceMoreActions">
|
||||
<stack class="zen-current-workspace-indicator-stack">
|
||||
<image class="zen-current-workspace-indicator-chevron" />
|
||||
<hbox class="zen-current-workspace-indicator-icon" />
|
||||
|
||||
@@ -11,7 +11,6 @@ const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
||||
@@ -157,11 +156,28 @@ class nsZenWorkspaces {
|
||||
this._invalidateBookmarkContainers();
|
||||
};
|
||||
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
|
||||
const onContainerDeleted = subject => {
|
||||
const userContextId = subject?.wrappedJSObject?.userContextId;
|
||||
for (const workspace of this.getWorkspaces()) {
|
||||
if (workspace.containerTabId === userContextId) {
|
||||
workspace.containerTabId = 0;
|
||||
this.saveWorkspace(workspace);
|
||||
}
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(
|
||||
onContainerDeleted,
|
||||
"contextual-identity-deleted"
|
||||
);
|
||||
window.addEventListener("unload", () => {
|
||||
Services.obs.removeObserver(
|
||||
observerFunction,
|
||||
"workspace-bookmarks-updated"
|
||||
);
|
||||
Services.obs.removeObserver(
|
||||
onContainerDeleted,
|
||||
"contextual-identity-deleted"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -173,63 +189,6 @@ class nsZenWorkspaces {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies live sync changes: updates workspace cache, removes deleted items,
|
||||
* then creates/updates pulled items.
|
||||
*
|
||||
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
|
||||
* @param {{ spaces: Array}} removals Items to remove.
|
||||
*/
|
||||
async _applySyncChanges(pulled, removals = {}) {
|
||||
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
|
||||
return;
|
||||
}
|
||||
await this.promiseInitialized;
|
||||
|
||||
// 1. Update workspace cache (remove deleted, merge pulled)
|
||||
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
|
||||
if (removedSpaceIds.size || pulled.spaces?.length) {
|
||||
const localMap = new Map(
|
||||
this.getWorkspaces()
|
||||
.filter(w => !removedSpaceIds.has(w.uuid))
|
||||
.map(w => [w.uuid, w])
|
||||
);
|
||||
for (const space of pulled.spaces || []) {
|
||||
if (!space?.uuid) {
|
||||
continue;
|
||||
}
|
||||
const existing = localMap.get(space.uuid);
|
||||
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
|
||||
}
|
||||
await this.propagateWorkspaces(
|
||||
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
|
||||
);
|
||||
this.#propagateWorkspaceData();
|
||||
}
|
||||
}
|
||||
|
||||
#getOrderedWorkspacesByPosition(workspaces) {
|
||||
return [...workspaces]
|
||||
.map((workspace, index) => ({ workspace, index }))
|
||||
.sort((a, b) => {
|
||||
const aPosition =
|
||||
typeof a.workspace.position === "number"
|
||||
? a.workspace.position
|
||||
: a.index;
|
||||
const bPosition =
|
||||
typeof b.workspace.position === "number"
|
||||
? b.workspace.position
|
||||
: b.index;
|
||||
return aPosition - bPosition || a.index - b.index;
|
||||
})
|
||||
.map(({ workspace }) => {
|
||||
// strip the position property that comes from pulled workspaces
|
||||
const rest = { ...workspace };
|
||||
delete rest.position;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
#afterLoadInit() {
|
||||
const onResize = (...args) => {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -862,13 +821,6 @@ class nsZenWorkspaces {
|
||||
})();
|
||||
}
|
||||
|
||||
#markWorkspaceChanged(workspaceId) {
|
||||
lazy.ZenSyncStore.markItemChanged({
|
||||
type: "space",
|
||||
id: workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async selectStartPage() {
|
||||
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
|
||||
return;
|
||||
@@ -1297,8 +1249,6 @@ class nsZenWorkspaces {
|
||||
} else {
|
||||
workspacesData.push(workspaceData);
|
||||
}
|
||||
// mark item as changed for sync
|
||||
this.#markWorkspaceChanged(workspaceData.uuid);
|
||||
|
||||
this.#propagateWorkspaceData();
|
||||
}
|
||||
@@ -1307,9 +1257,6 @@ class nsZenWorkspaces {
|
||||
let { promise, resolve } = Promise.withResolvers();
|
||||
this.#deleteWorkspaceOwnedTabs(windowID);
|
||||
|
||||
// mark item as changed for sync
|
||||
this.#markWorkspaceChanged(windowID);
|
||||
|
||||
let workspacesData = this.getWorkspaces();
|
||||
// Remove the workspace from the cache
|
||||
workspacesData = workspacesData.filter(
|
||||
@@ -1442,11 +1389,6 @@ class nsZenWorkspaces {
|
||||
return;
|
||||
}
|
||||
const workspaces = this.getWorkspaces();
|
||||
// Track previous positions so we only notify observers for workspaces whose
|
||||
// position changed during the reorder.
|
||||
const previousPositions = new Map(
|
||||
workspaces.map((workspace, index) => [workspace.uuid, index])
|
||||
);
|
||||
|
||||
const workspace = workspaces.find(w => w.uuid === id);
|
||||
if (!workspace) {
|
||||
@@ -1475,15 +1417,6 @@ class nsZenWorkspaces {
|
||||
// Propagate the changes if the order has changed
|
||||
if (currentIndex !== newPosition) {
|
||||
this._workspaceCache = workspaces;
|
||||
|
||||
for (const [i, ws] of workspaces.entries()) {
|
||||
if (previousPositions.get(ws.uuid) === i) {
|
||||
continue;
|
||||
}
|
||||
// mark item as changed for sync
|
||||
this.#markWorkspaceChanged(ws.uuid);
|
||||
}
|
||||
|
||||
this.#propagateWorkspaceData(workspaces);
|
||||
}
|
||||
}
|
||||
@@ -1790,6 +1723,15 @@ class nsZenWorkspaces {
|
||||
}
|
||||
|
||||
makeSureEmptyTabIsFirst() {
|
||||
if (
|
||||
gZenUIManager.testingEnabled &&
|
||||
this._emptyTab &&
|
||||
(this._emptyTab.closing || !this._emptyTab.isConnected)
|
||||
) {
|
||||
this._emptyTab = gBrowser.tabs.find(
|
||||
tab => tab.hasAttribute("zen-empty-tab") && !tab.closing
|
||||
);
|
||||
}
|
||||
const emptyTab = this._emptyTab;
|
||||
if (emptyTab) {
|
||||
emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace);
|
||||
@@ -3320,8 +3262,10 @@ class nsZenWorkspaces {
|
||||
return width;
|
||||
}, 0);
|
||||
|
||||
// Check if the total width exceeds the parent's width
|
||||
if (totalWidth > parent.clientWidth) {
|
||||
// Check if the total width exceeds the parent's width.
|
||||
const parentWidth =
|
||||
window.windowUtils.getBoundsWithoutFlushing(parent).width;
|
||||
if (totalWidth > parentWidth) {
|
||||
parent.setAttribute("icons-overflow", "true");
|
||||
} else {
|
||||
parent.removeAttribute("icons-overflow");
|
||||
@@ -3329,7 +3273,7 @@ class nsZenWorkspaces {
|
||||
|
||||
// Set the width of each icon to the maximum size they can fit on
|
||||
const widthPerButton = Math.max(
|
||||
(parent.clientWidth - separation * (parent.children.length - 1)) /
|
||||
(parentWidth - separation * (parent.children.length - 1)) /
|
||||
parent.children.length,
|
||||
minButtonSize
|
||||
);
|
||||
|
||||
@@ -231,6 +231,13 @@
|
||||
width: var(--zen-thumb-width);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transform: scale(1);
|
||||
transform-origin: center;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
&:hover::-moz-range-thumb {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
@@ -338,7 +345,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not([disabled]):hover {
|
||||
background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
& toolbarbutton {
|
||||
margin: 0;
|
||||
max-width: 28px;
|
||||
border-radius: var(--toolbarbutton-border-radius);
|
||||
height: 28px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -230,7 +230,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
this.removeGroup(groupIndex);
|
||||
if (changeTab) {
|
||||
gBrowser.selectedTab = remainingTabs[0];
|
||||
const tabToSelect = remainingTabs.find(remaining => remaining !== tab);
|
||||
if (tabToSelect) {
|
||||
gBrowser.selectedTab = tabToSelect;
|
||||
}
|
||||
document
|
||||
.getElementById("cmd_zenNewEmptySplit")
|
||||
.removeAttribute("disabled");
|
||||
@@ -355,7 +358,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const currentView = this._data[this._lastOpenedTab.splitViewValue];
|
||||
const currentView = this._data.find(group =>
|
||||
group.tabs.includes(this._lastOpenedTab)
|
||||
);
|
||||
if (currentView?.tabs.length >= this.MAX_TABS) {
|
||||
return;
|
||||
}
|
||||
@@ -540,7 +545,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
const panelsWidth = panelsRect.width;
|
||||
const panelsHeight = panelsRect.height;
|
||||
let numOfTabsToDivide = 2;
|
||||
const currentView = this._data[this._lastOpenedTab.splitViewValue];
|
||||
const currentView = this._data.find(group =>
|
||||
group.tabs.includes(this._lastOpenedTab)
|
||||
);
|
||||
if (currentView) {
|
||||
numOfTabsToDivide = currentView.tabs.length + 1;
|
||||
}
|
||||
@@ -1103,7 +1110,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
*/
|
||||
resetTabState(tab, forUnsplit) {
|
||||
tab.splitView = false;
|
||||
delete tab.splitViewValue;
|
||||
tab.removeAttribute("split-view");
|
||||
tab.linkedBrowser.zenModeActive = false;
|
||||
const container = tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||
@@ -1147,6 +1153,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
if (this.currentView === groupIndex) {
|
||||
this.deactivateCurrentSplitView();
|
||||
} else if (this.currentView > groupIndex) {
|
||||
this.currentView--;
|
||||
}
|
||||
for (const tab of this._data[groupIndex].tabs) {
|
||||
this.resetTabState(tab, true);
|
||||
@@ -1638,7 +1646,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
applyGridToTabs(tabs) {
|
||||
tabs.forEach(tab => {
|
||||
tab.splitView = true;
|
||||
tab.splitViewValue = this.currentView;
|
||||
tab.setAttribute("split-view", "true");
|
||||
const container = tab.linkedBrowser?.closest(".browserSidebarContainer");
|
||||
container.setAttribute("is-zen-split", "true");
|
||||
@@ -2530,22 +2537,33 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
this.activateSplitView(data);
|
||||
gBrowser.selectedTab = emptyTab;
|
||||
setTimeout(() => {
|
||||
const closeToken = {};
|
||||
const controller = new AbortController();
|
||||
const cleanup = (onSwitch = false, groupIndex = null) => {
|
||||
if (groupIndex === null) {
|
||||
groupIndex = this._data.findIndex(group =>
|
||||
group.tabs.includes(emptyTab)
|
||||
);
|
||||
}
|
||||
this.removeTabFromGroup(emptyTab, groupIndex, {
|
||||
changeTab: !onSwitch,
|
||||
forUnsplit: true,
|
||||
});
|
||||
const command = document.getElementById("cmd_zenNewEmptySplit");
|
||||
command.removeAttribute("disabled");
|
||||
};
|
||||
window.addEventListener(
|
||||
"ZenURLBarClosed",
|
||||
event => {
|
||||
if (!gZenUIManager.matchesCloseToken(closeToken, event)) {
|
||||
return;
|
||||
}
|
||||
controller.abort();
|
||||
const { onElementPicked, onSwitch } = event.detail;
|
||||
const groupIndex = this._data.findIndex(group =>
|
||||
group.tabs.includes(emptyTab)
|
||||
);
|
||||
const newSelectedTab = gBrowser.selectedTab;
|
||||
const cleanup = () => {
|
||||
this.removeTabFromGroup(emptyTab, groupIndex, {
|
||||
changeTab: !onSwitch,
|
||||
forUnsplit: true,
|
||||
});
|
||||
const command = document.getElementById("cmd_zenNewEmptySplit");
|
||||
command.removeAttribute("disabled");
|
||||
};
|
||||
if (onElementPicked) {
|
||||
if (
|
||||
newSelectedTab === emptyTab ||
|
||||
@@ -2553,7 +2571,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
selectedTab.getAttribute("zen-workspace-id") !==
|
||||
newSelectedTab.getAttribute("zen-workspace-id")
|
||||
) {
|
||||
cleanup();
|
||||
cleanup(onSwitch, groupIndex);
|
||||
return;
|
||||
}
|
||||
this.removeTabFromGroup(emptyTab, groupIndex, {
|
||||
@@ -2569,12 +2587,17 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
topOrLeft ? 0 : 1
|
||||
);
|
||||
} else {
|
||||
cleanup();
|
||||
cleanup(onSwitch, groupIndex);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
gZenUIManager.handleNewTab(false, false, "tab", true);
|
||||
if (
|
||||
!gZenUIManager.handleNewTab(false, false, "tab", true, closeToken)
|
||||
) {
|
||||
controller.abort();
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
182
src/zen/sync/ZenSpacesSync.sys.mjs
Normal file
182
src/zen/sync/ZenSpacesSync.sys.mjs
Normal file
@@ -0,0 +1,182 @@
|
||||
/* 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 {
|
||||
Store,
|
||||
SyncEngine,
|
||||
Tracker,
|
||||
} from "resource://services-sync/engines.sys.mjs";
|
||||
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
|
||||
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
|
||||
import {
|
||||
SIDEBAR_COLLECTED_TOPIC,
|
||||
syncLog,
|
||||
ZenSpacesSyncModel,
|
||||
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenSpacesSyncApplier: "resource:///modules/zen/ZenSpacesSyncApplier.sys.mjs",
|
||||
});
|
||||
|
||||
const TRACKED_TOPICS = [
|
||||
SIDEBAR_COLLECTED_TOPIC,
|
||||
"contextual-identity-created",
|
||||
"contextual-identity-updated",
|
||||
"contextual-identity-deleted",
|
||||
];
|
||||
|
||||
export class ZenSpacesSyncRecord extends CryptoWrapper {
|
||||
_logName = "Sync.Record.ZenSpaces";
|
||||
}
|
||||
|
||||
ZenSpacesSyncRecord.prototype.type = "spaces";
|
||||
|
||||
/**
|
||||
* Store for the Spaces engine. Outgoing records are pure projections of
|
||||
* the session-store sidebar data; incoming batches are handed to the
|
||||
* applier, which reconciles them into the live browser.
|
||||
*/
|
||||
class ZenSpacesSyncStore extends Store {
|
||||
async getAllIDs() {
|
||||
return ZenSpacesSyncModel.getAllRecordIds();
|
||||
}
|
||||
|
||||
async itemExists(id) {
|
||||
return ZenSpacesSyncModel.itemExists(id);
|
||||
}
|
||||
|
||||
async createRecord(id, collection) {
|
||||
const record = new ZenSpacesSyncRecord(collection, id);
|
||||
const projected = ZenSpacesSyncModel.projectRecord(id);
|
||||
if (!projected) {
|
||||
record.deleted = true;
|
||||
return record;
|
||||
}
|
||||
record.cleartext = { id, kind: projected.kind, data: projected.data };
|
||||
return record;
|
||||
}
|
||||
|
||||
async applyIncomingBatch(records, countTelemetry) {
|
||||
const failed = await lazy.ZenSpacesSyncApplier.applyBatch(records);
|
||||
for (const id of failed) {
|
||||
countTelemetry?.addIncomingFailedReason(`failed to apply ${id}`);
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
async wipe() {
|
||||
// Never delete user data on an engine wipe.
|
||||
}
|
||||
|
||||
async changeItemID() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracker for the Spaces engine. There is no per-event bookkeeping: every
|
||||
* time fresh sidebar data is collected (or a container changes), the model
|
||||
* diffs the current projections against the last-uploaded snapshot; the
|
||||
* score is only bumped when something actually differs.
|
||||
*/
|
||||
class ZenSpacesSyncTracker extends Tracker {
|
||||
_ignoreAll = false;
|
||||
|
||||
get ignoreAll() {
|
||||
return this._ignoreAll;
|
||||
}
|
||||
|
||||
set ignoreAll(value) {
|
||||
this._ignoreAll = value;
|
||||
}
|
||||
|
||||
onStart() {
|
||||
for (const topic of TRACKED_TOPICS) {
|
||||
Services.obs.addObserver(this, topic);
|
||||
}
|
||||
}
|
||||
|
||||
onStop() {
|
||||
for (const topic of TRACKED_TOPICS) {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
}
|
||||
}
|
||||
|
||||
observe(subject, topic) {
|
||||
if (this.ignoreAll) {
|
||||
return;
|
||||
}
|
||||
if (topic !== SIDEBAR_COLLECTED_TOPIC) {
|
||||
ZenSpacesSyncModel.invalidate();
|
||||
}
|
||||
try {
|
||||
if (ZenSpacesSyncModel.hasPendingChanges()) {
|
||||
syncLog(`tracker: pending changes after ${topic}, requesting sync`);
|
||||
this.score += SCORE_INCREMENT_XLARGE;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("ZenSpacesSync: change detection failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ZenSpacesSyncEngine extends SyncEngine {
|
||||
constructor(service) {
|
||||
super("Spaces", service);
|
||||
}
|
||||
|
||||
get _storeObj() {
|
||||
return ZenSpacesSyncStore;
|
||||
}
|
||||
|
||||
get _trackerObj() {
|
||||
return ZenSpacesSyncTracker;
|
||||
}
|
||||
|
||||
get _recordObj() {
|
||||
return ZenSpacesSyncRecord;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
get syncPriority() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
get allowSkippedRecord() {
|
||||
// A record that persistently fails to upload is skipped; the snapshot
|
||||
// diff retries it on every following sync anyway.
|
||||
return true;
|
||||
}
|
||||
|
||||
async getChangedIDs() {
|
||||
return ZenSpacesSyncModel.computeChangedIDs();
|
||||
}
|
||||
|
||||
async _reconcile(item) {
|
||||
// Incoming records always apply, and any local divergence re-uploads afterwards
|
||||
// (see ZenSpacesSyncModel.noteApplied). The base reconciliation would
|
||||
// instead drop incoming changes whenever the id is also in the local
|
||||
// changed set, because the snapshot diff stamps its changes with "now"
|
||||
// and the local side would always win the age comparison.
|
||||
//
|
||||
// An id we accept must also leave the outgoing set. Projections only
|
||||
// reflect the applied state after the delayed session collection, so
|
||||
// uploading the id later in this same sync would overwrite the record
|
||||
// we just accepted with the stale pre-apply state (and the other
|
||||
// device would then apply that, endlessly trading states back and
|
||||
// forth). Divergence that survives the collection re-uploads on a
|
||||
// later sync through the snapshot diff.
|
||||
this._modified.delete(item.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async trackRemainingChanges() {}
|
||||
|
||||
async _onRecordsWritten(succeeded) {
|
||||
ZenSpacesSyncModel.markUploaded(succeeded);
|
||||
}
|
||||
}
|
||||
916
src/zen/sync/ZenSpacesSyncApplier.sys.mjs
Normal file
916
src/zen/sync/ZenSpacesSyncApplier.sys.mjs
Normal file
@@ -0,0 +1,916 @@
|
||||
/* 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 {
|
||||
canonicalJSON,
|
||||
LAYOUT_RECORD_ID,
|
||||
RECORD_KINDS,
|
||||
syncableIconUrl,
|
||||
syncLog,
|
||||
ZenSpacesSyncModel,
|
||||
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
|
||||
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
||||
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
|
||||
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
||||
ZenLiveFoldersManager:
|
||||
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
||||
ContextualIdentityService:
|
||||
"moz-src:///toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs",
|
||||
});
|
||||
|
||||
const FIRST_SYNC_ANIMATION_PREF = "zen.spaces-sync.first-sync-animation-shown";
|
||||
|
||||
/**
|
||||
* Applies batches of incoming Spaces records to the local browser.
|
||||
*/
|
||||
class nsZenSpacesSyncApplier {
|
||||
#itemIn(win, id) {
|
||||
return id ? win.document.getElementById(id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a record's container guid to the local userContextId. A guid with
|
||||
* no local mapping (its container record hasn't applied here yet) throws,
|
||||
* failing the record so it redelivers once the container exists .
|
||||
*
|
||||
* @param {?string} containerGuid
|
||||
* @returns {number}
|
||||
*/
|
||||
#resolveContainerId(containerGuid) {
|
||||
if (!containerGuid) {
|
||||
return 0;
|
||||
}
|
||||
const mapped = ZenSpacesSyncModel.contextIdForGuid(containerGuid);
|
||||
if (mapped === null) {
|
||||
throw new Error(`unknown container guid ${containerGuid}`);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} records - decrypted incoming records.
|
||||
* @returns {Array<string>} ids of records that failed to apply.
|
||||
*/
|
||||
async applyBatch(records) {
|
||||
const incoming = {
|
||||
containers: [],
|
||||
spaces: [],
|
||||
tabs: [],
|
||||
folders: [],
|
||||
splits: [],
|
||||
layout: null,
|
||||
deleted: [],
|
||||
};
|
||||
const handled = new Set();
|
||||
|
||||
for (const record of records) {
|
||||
if (typeof record.id !== "string" || !record.id) {
|
||||
continue;
|
||||
}
|
||||
if (record.deleted) {
|
||||
if (record.id !== LAYOUT_RECORD_ID) {
|
||||
incoming.deleted.push({ key: record.id, record });
|
||||
}
|
||||
handled.add(record);
|
||||
continue;
|
||||
}
|
||||
const data = record.cleartext?.data;
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
const entry = { key: record.id, data, record };
|
||||
switch (record.cleartext.kind) {
|
||||
case RECORD_KINDS.CONTAINER:
|
||||
incoming.containers.push(entry);
|
||||
break;
|
||||
case RECORD_KINDS.SPACE:
|
||||
incoming.spaces.push(entry);
|
||||
break;
|
||||
case RECORD_KINDS.TAB:
|
||||
incoming.tabs.push(entry);
|
||||
break;
|
||||
case RECORD_KINDS.FOLDER:
|
||||
incoming.folders.push(entry);
|
||||
break;
|
||||
case RECORD_KINDS.SPLIT:
|
||||
incoming.splits.push(entry);
|
||||
break;
|
||||
case RECORD_KINDS.LAYOUT:
|
||||
incoming.layout = entry;
|
||||
break;
|
||||
default:
|
||||
// Unknown kind (newer client): leave untouched and unacknowledged.
|
||||
continue;
|
||||
}
|
||||
handled.add(record);
|
||||
}
|
||||
|
||||
syncLog(
|
||||
`incoming batch: ${incoming.containers.length} containers, ` +
|
||||
`${incoming.spaces.length} spaces, ${incoming.tabs.length} tabs, ` +
|
||||
`${incoming.folders.length} folders, ${incoming.splits.length} splits, ` +
|
||||
`layout=${!!incoming.layout}, ${incoming.deleted.length} tombstones`
|
||||
);
|
||||
|
||||
const failed = new Set();
|
||||
const fail = (record, e) => {
|
||||
failed.add(record.id);
|
||||
console.error(`ZenSpacesSync: failed to apply ${record.id}:`, e);
|
||||
};
|
||||
|
||||
const deletions = this.#applyContainers(incoming, fail);
|
||||
|
||||
const win = lazy.ZenWindowSync.firstSyncedWindow;
|
||||
if (!win) {
|
||||
// Report the records as failed so the engine redelivers the batch on a later sync.
|
||||
const noWindow = new Error("no synced window to apply into");
|
||||
for (const entry of [
|
||||
...incoming.spaces,
|
||||
...incoming.folders,
|
||||
...incoming.tabs,
|
||||
...incoming.splits,
|
||||
...(incoming.layout ? [incoming.layout] : []),
|
||||
...deletions,
|
||||
]) {
|
||||
fail(entry.record, noWindow);
|
||||
}
|
||||
} else {
|
||||
await win.gZenWorkspaces.promiseInitialized;
|
||||
this.#maybePlayFirstSyncAnimation(win);
|
||||
const removals = this.#routeTombstones(win, deletions);
|
||||
this.#deleteTabs(win, removals.tabs, fail);
|
||||
this.#deleteSplits(win, removals.splits, fail);
|
||||
await this.#applySpaces(win, incoming.spaces, fail);
|
||||
await this.#applyFolders(win, incoming.folders, fail);
|
||||
this.#applyTabs(win, incoming.tabs, fail);
|
||||
this.#applySplits(win, incoming.splits, fail);
|
||||
await this.#deleteFolders(win, removals.folders, fail);
|
||||
await this.#deleteSpaces(win, removals.spaces, fail);
|
||||
this.#applyOrdering(win, incoming, fail);
|
||||
// Collect the session soon so the stored sidebar (and with it the
|
||||
// sync projections) reflects the applied state instead of re-uploading
|
||||
// the pre-apply one.
|
||||
lazy.SessionSaver.runDelayed();
|
||||
}
|
||||
|
||||
ZenSpacesSyncModel.invalidate();
|
||||
for (const record of records) {
|
||||
if (!handled.has(record) || failed.has(record.id)) {
|
||||
continue;
|
||||
}
|
||||
ZenSpacesSyncModel.noteApplied(
|
||||
record.id,
|
||||
record.deleted ? null : record.cleartext
|
||||
);
|
||||
}
|
||||
return [...failed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the update sweep animation over the window the first time
|
||||
* incoming sync data lands on this profile.
|
||||
*
|
||||
* @param {Window} win
|
||||
*/
|
||||
#maybePlayFirstSyncAnimation(win) {
|
||||
if (Services.prefs.getBoolPref(FIRST_SYNC_ANIMATION_PREF, false)) {
|
||||
return;
|
||||
}
|
||||
Services.prefs.setBoolPref(FIRST_SYNC_ANIMATION_PREF, true);
|
||||
win.gZenStartup.playWindowSweepAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tombstones have no payload. Whatever local thing owns the id decides
|
||||
* what kind of deletion this is. Ids with no local counterpart need no
|
||||
* work at all.
|
||||
*
|
||||
* @param {Window} win
|
||||
* @param {Array<{key: string, record: object}>} deletions
|
||||
*/
|
||||
#routeTombstones(win, deletions) {
|
||||
const routed = { tabs: [], folders: [], splits: [], spaces: [] };
|
||||
const spaces = win.gZenWorkspaces.getWorkspaces();
|
||||
for (const entry of deletions) {
|
||||
const el = this.#itemIn(win, entry.key);
|
||||
if (win.gBrowser.isTab(el)) {
|
||||
routed.tabs.push(entry);
|
||||
} else if (el?.isZenFolder) {
|
||||
routed.folders.push(entry);
|
||||
} else if (el?.hasAttribute?.("split-view-group")) {
|
||||
routed.splits.push(entry);
|
||||
} else if (spaces.some(s => s.uuid === entry.key)) {
|
||||
routed.spaces.push(entry);
|
||||
}
|
||||
}
|
||||
if (deletions.length) {
|
||||
const matched = new Set([
|
||||
...routed.tabs,
|
||||
...routed.folders,
|
||||
...routed.splits,
|
||||
...routed.spaces,
|
||||
]);
|
||||
syncLog("tombstones routed:", {
|
||||
tabs: routed.tabs.map(e => e.key),
|
||||
folders: routed.folders.map(e => e.key),
|
||||
splits: routed.splits.map(e => e.key),
|
||||
spaces: routed.spaces.map(e => e.key),
|
||||
unmatched: deletions.filter(e => !matched.has(e)).map(e => e.key),
|
||||
});
|
||||
}
|
||||
return routed;
|
||||
}
|
||||
|
||||
/* Mark: containers */
|
||||
|
||||
#applyContainers(incoming, fail) {
|
||||
for (const { key: guid, data, record } of incoming.containers) {
|
||||
try {
|
||||
if (!data.name) {
|
||||
continue;
|
||||
}
|
||||
const mapped = ZenSpacesSyncModel.contextIdForGuid(guid);
|
||||
const existing =
|
||||
mapped !== null
|
||||
? lazy.ContextualIdentityService.getPublicIdentityFromId(mapped)
|
||||
: null;
|
||||
if (existing) {
|
||||
lazy.ContextualIdentityService.update(
|
||||
mapped,
|
||||
data.name,
|
||||
data.icon,
|
||||
data.color
|
||||
);
|
||||
} else {
|
||||
const identity = lazy.ContextualIdentityService.create(
|
||||
data.name,
|
||||
data.icon,
|
||||
data.color
|
||||
);
|
||||
ZenSpacesSyncModel.registerContainerGuid(
|
||||
guid,
|
||||
identity.userContextId
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
|
||||
const rest = [];
|
||||
for (const entry of incoming.deleted) {
|
||||
const mapped = ZenSpacesSyncModel.contextIdForGuid(entry.key);
|
||||
if (mapped === null) {
|
||||
rest.push(entry);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (lazy.ContextualIdentityService.getPublicIdentityFromId(mapped)) {
|
||||
lazy.ContextualIdentityService.remove(mapped);
|
||||
}
|
||||
ZenSpacesSyncModel.forgetContainerGuid(entry.key);
|
||||
} catch (e) {
|
||||
fail(entry.record, e);
|
||||
}
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
|
||||
/* Mark: spaces */
|
||||
|
||||
async #applySpaces(win, spaces, fail) {
|
||||
if (!spaces.length) {
|
||||
return;
|
||||
}
|
||||
const list = win.gZenWorkspaces.getWorkspaces();
|
||||
let changed = false;
|
||||
const repaint = [];
|
||||
for (const { data, record } of spaces) {
|
||||
try {
|
||||
if (!data.uuid) {
|
||||
continue;
|
||||
}
|
||||
const fields = {
|
||||
uuid: data.uuid,
|
||||
name: data.name ?? "",
|
||||
icon: data.icon ?? undefined,
|
||||
theme: data.theme ?? null,
|
||||
containerTabId: this.#resolveContainerId(data.containerGuid),
|
||||
};
|
||||
// A space record re-syncs whenever its child ordering changes, so
|
||||
// only propagate (and especially repaint) when its own fields did.
|
||||
const index = list.findIndex(s => s.uuid === data.uuid);
|
||||
const current = index === -1 ? null : list[index];
|
||||
const visualChanged =
|
||||
!current ||
|
||||
(current.name ?? "") !== fields.name ||
|
||||
(current.icon ?? null) !== (fields.icon ?? null) ||
|
||||
canonicalJSON(current.theme ?? null) !==
|
||||
canonicalJSON(fields.theme ?? null);
|
||||
const containerChanged =
|
||||
!current || (current.containerTabId ?? 0) !== fields.containerTabId;
|
||||
if (containerChanged) {
|
||||
syncLog(
|
||||
`space ${data.uuid} containerTabId ` +
|
||||
`${current?.containerTabId ?? 0} -> ${fields.containerTabId} ` +
|
||||
`(guid ${data.containerGuid ?? "none"})`
|
||||
);
|
||||
}
|
||||
if (!current) {
|
||||
list.push(fields);
|
||||
changed = true;
|
||||
} else if (visualChanged || containerChanged) {
|
||||
list[index] = { ...current, ...fields };
|
||||
changed = true;
|
||||
}
|
||||
if (visualChanged) {
|
||||
repaint.push(data.uuid);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await win.gZenWorkspaces.propagateWorkspaces(list);
|
||||
win.gZenWindowSync.propagateWorkspacesToAllWindows(list);
|
||||
for (const uuid of repaint) {
|
||||
const updated = list.find(s => s.uuid === uuid);
|
||||
if (updated) {
|
||||
win.gZenThemePicker.onWorkspaceChange(updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #deleteSpaces(win, removals, fail) {
|
||||
for (const { key: uuid, record } of removals) {
|
||||
try {
|
||||
const spaces = win.gZenWorkspaces.getWorkspaces();
|
||||
if (!spaces.some(s => s.uuid === uuid)) {
|
||||
continue;
|
||||
}
|
||||
if (spaces.length <= 1) {
|
||||
// Never delete the last space; the local copy revives it remotely.
|
||||
continue;
|
||||
}
|
||||
if (!(await this.#confirmRemoteSpaceDelete(win, uuid))) {
|
||||
continue;
|
||||
}
|
||||
await win.gZenWorkspaces.removeWorkspace(uuid);
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Window} win
|
||||
* @param {string} uuid
|
||||
* @returns {Promise<boolean>} Whether the user confirmed the deletion.
|
||||
*/
|
||||
async #confirmRemoteSpaceDelete(win, uuid) {
|
||||
const name = win.gZenWorkspaces.getWorkspaceFromId(uuid)?.name || uuid;
|
||||
const [title, body] = await win.document.l10n.formatValues([
|
||||
{ id: "zen-workspaces-remote-delete-title" },
|
||||
{ id: "zen-workspaces-remote-delete-body", args: { name } },
|
||||
]);
|
||||
const result = await Services.prompt.asyncConfirmEx(
|
||||
win.browsingContext,
|
||||
Services.prompt.MODAL_TYPE_WINDOW,
|
||||
title,
|
||||
body,
|
||||
Services.prompt.STD_YES_NO_BUTTONS,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
);
|
||||
return result.get("buttonNumClicked") === 0;
|
||||
}
|
||||
|
||||
/* Mark: folders */
|
||||
|
||||
async #applyFolders(win, folders, fail) {
|
||||
// Parents before children so nesting targets exist.
|
||||
const depths = new Map(folders.map(f => [f.key, f.data.parentFolderId]));
|
||||
const depthOf = key => {
|
||||
let depth = 0;
|
||||
let parent = depths.get(key);
|
||||
while (parent && depths.has(parent) && depth < 10) {
|
||||
depth++;
|
||||
parent = depths.get(parent);
|
||||
}
|
||||
return depth;
|
||||
};
|
||||
const ordered = [...folders].sort(
|
||||
(a, b) => depthOf(a.key) - depthOf(b.key)
|
||||
);
|
||||
|
||||
for (const { key: folderId, data, record } of ordered) {
|
||||
try {
|
||||
let folder = this.#itemIn(win, folderId);
|
||||
if (!folder?.isZenFolder) {
|
||||
folder = win.gZenFolders.createFolder([], {
|
||||
id: folderId,
|
||||
label: data.name || "Folder",
|
||||
workspaceId:
|
||||
data.workspaceUuid || win.gZenWorkspaces.activeWorkspace,
|
||||
isLiveFolder: !!data.live,
|
||||
});
|
||||
} else if (data.name && folder.label !== data.name) {
|
||||
folder.label = data.name;
|
||||
}
|
||||
|
||||
if (data.live && !folder.isLiveFolder) {
|
||||
// The folder synced before its provider config was available and
|
||||
// materialized as a plain folder; upgrade it so this profile
|
||||
// projects the config too instead of clobbering it with live: null.
|
||||
folder.isLiveFolder = true;
|
||||
}
|
||||
|
||||
if ((folder.iconURL || null) !== (data.icon || null)) {
|
||||
win.gZenFolders.setFolderUserIcon(folder, data.icon || null);
|
||||
// Window sync picks this up and mirrors the icon everywhere else.
|
||||
folder.dispatchEvent(
|
||||
new win.CustomEvent("TabGroupUpdate", { bubbles: true })
|
||||
);
|
||||
}
|
||||
|
||||
const desiredParent = data.parentFolderId
|
||||
? this.#itemIn(win, data.parentFolderId)
|
||||
: null;
|
||||
const currentParent = folder.group;
|
||||
if ((currentParent?.id || null) !== (data.parentFolderId || null)) {
|
||||
if (desiredParent?.isZenFolder) {
|
||||
win.gBrowser.zenHandleTabMove(folder, () => {
|
||||
if (desiredParent.tabs.length) {
|
||||
desiredParent.tabs[0].after(folder);
|
||||
} else {
|
||||
desiredParent.appendChild(folder);
|
||||
}
|
||||
});
|
||||
} else if (!data.parentFolderId && currentParent?.isZenFolder) {
|
||||
const ws = data.workspaceUuid || win.gZenWorkspaces.activeWorkspace;
|
||||
const container =
|
||||
win.gZenWorkspaces.workspaceElement(ws)?.pinnedTabsContainer;
|
||||
if (container) {
|
||||
win.gBrowser.zenHandleTabMove(folder, () => {
|
||||
container.insertBefore(
|
||||
folder,
|
||||
container.querySelector(".pinned-tabs-container-separator")
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
data.workspaceUuid &&
|
||||
folder.getAttribute("zen-workspace-id") !== data.workspaceUuid
|
||||
) {
|
||||
if (!folder.group) {
|
||||
win.gZenFolders.changeFolderToSpace(folder, data.workspaceUuid);
|
||||
} else {
|
||||
folder.setAttribute("zen-workspace-id", data.workspaceUuid);
|
||||
}
|
||||
}
|
||||
if (data.live) {
|
||||
await lazy.ZenLiveFoldersManager.adoptSyncedFolder(
|
||||
folderId,
|
||||
data.live
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #deleteFolders(win, removals, fail) {
|
||||
for (const { key: folderId, record } of removals) {
|
||||
try {
|
||||
const folder = this.#itemIn(win, folderId);
|
||||
if (!folder?.isZenFolder) {
|
||||
continue;
|
||||
}
|
||||
// Members without their own tombstone survive: unpack, then delete
|
||||
// the (now empty) folder.
|
||||
await folder.unpackTabs();
|
||||
await folder.delete();
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark: tabs */
|
||||
|
||||
#applyTabs(win, tabs, fail) {
|
||||
for (const { key: tabId, data, record } of tabs) {
|
||||
try {
|
||||
if (!data.url || data.url === "about:blank") {
|
||||
continue;
|
||||
}
|
||||
const existing = this.#itemIn(win, tabId);
|
||||
if (win.gBrowser.isTab(existing)) {
|
||||
this.#updateTab(win, existing, data);
|
||||
} else {
|
||||
this.#createTab(win, tabId, data);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#createTab(win, tabId, data) {
|
||||
const userContextId = this.#resolveContainerId(data.containerGuid);
|
||||
syncLog(
|
||||
`creating tab ${tabId} ` +
|
||||
`(essential=${!!data.essential}, container=${userContextId})`,
|
||||
data.url
|
||||
);
|
||||
const tab = win.gBrowser.addTrustedTab(data.url, {
|
||||
createLazyBrowser: true,
|
||||
inBackground: true,
|
||||
skipAnimation: true,
|
||||
skipBackgroundNotify: true,
|
||||
lazyTabTitle: data.title || undefined,
|
||||
userContextId,
|
||||
skipRoute: true,
|
||||
});
|
||||
// Setting the sync id before the queued TabOpen handler runs makes
|
||||
// window sync treat this tab as already replicated.
|
||||
tab.id = tabId;
|
||||
tab._zenContentsVisible = true;
|
||||
this.#updateTabIdentity(win, tab, data);
|
||||
if (data.essential) {
|
||||
win.gZenPinnedTabManager.addToEssentials(tab, { replicating: true });
|
||||
} else {
|
||||
if (data.workspaceUuid) {
|
||||
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
|
||||
}
|
||||
win.gBrowser.pinTab(tab);
|
||||
if (data.workspaceUuid) {
|
||||
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
||||
}
|
||||
const folder = data.folderId && this.#itemIn(win, data.folderId);
|
||||
if (folder?.isZenFolder) {
|
||||
folder.addTabs([tab]);
|
||||
}
|
||||
}
|
||||
lazy.ZenWindowSync.on_TabOpen({ target: tab }, { ignoreExistingId: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciles the tab's synced identity: pinned initial state, static
|
||||
* label/icon, the live icon and the default-container marker.
|
||||
*
|
||||
* @param {Window} win
|
||||
* @param {MozTabbrowserTab} tab
|
||||
* @param {object} data
|
||||
*/
|
||||
#updateTabIdentity(win, tab, data) {
|
||||
const icon = syncableIconUrl(data.icon);
|
||||
const initial = tab._zenPinnedInitialState;
|
||||
const identityChanged =
|
||||
initial?.entry?.url !== data.url ||
|
||||
(initial?.entry?.title || "") !== (data.title || "");
|
||||
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
|
||||
lazy.ZenWindowSync.setPinnedInitialState(
|
||||
tab,
|
||||
{ url: data.url, title: data.title || "" },
|
||||
icon || undefined
|
||||
);
|
||||
}
|
||||
if (identityChanged && !tab.linkedPanel) {
|
||||
// An unloaded tab has no live state to lose: retarget its lazy session
|
||||
// state so the next activation loads the new canonical url directly
|
||||
// instead of whatever the tab was created with.
|
||||
this.#retargetUnloadedTab(win, tab, data, initial?.entry?.url);
|
||||
}
|
||||
|
||||
const staticLabel =
|
||||
typeof data.staticLabel === "string" ? data.staticLabel : null;
|
||||
const localLabel =
|
||||
typeof tab.zenStaticLabel === "string" ? tab.zenStaticLabel : null;
|
||||
if (staticLabel !== localLabel) {
|
||||
tab.zenStaticLabel = staticLabel ?? undefined;
|
||||
if (staticLabel) {
|
||||
tab._zenChangeLabelFlag = true;
|
||||
try {
|
||||
win.gBrowser._setTabLabel(tab, staticLabel);
|
||||
} finally {
|
||||
delete tab._zenChangeLabelFlag;
|
||||
}
|
||||
}
|
||||
}
|
||||
tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined;
|
||||
const iconDiffers =
|
||||
icon && syncableIconUrl(tab.getAttribute("image")) !== icon;
|
||||
if (iconDiffers && (data.hasStaticIcon || !tab.linkedPanel)) {
|
||||
try {
|
||||
syncLog(
|
||||
`setting synced${data.hasStaticIcon ? " static" : ""} ` +
|
||||
`icon on tab ${tab.id}`
|
||||
);
|
||||
win.gBrowser.setIcon(tab, icon);
|
||||
lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, {
|
||||
image: null,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("ZenSpacesSync: failed to set tab icon", e);
|
||||
}
|
||||
}
|
||||
if (data.defaultContainer) {
|
||||
tab.setAttribute("zenDefaultUserContextId", "true");
|
||||
} else {
|
||||
tab.removeAttribute("zenDefaultUserContextId");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Points an unloaded tab's session state at the record's canonical url,
|
||||
* mirroring ZenPinnedTabManager's reset-to-stored-state shape.
|
||||
*
|
||||
* @param {Window} win
|
||||
* @param {MozTabbrowserTab} tab
|
||||
* @param {object} data
|
||||
* @param {?string} previousPinUrl - The pin url before this record applied.
|
||||
*/
|
||||
#retargetUnloadedTab(win, tab, data, previousPinUrl) {
|
||||
try {
|
||||
const state = JSON.parse(win.SessionStore.getTabState(tab));
|
||||
const entries = state.entries || [];
|
||||
let currentUrl = null;
|
||||
if (entries.length) {
|
||||
const index = Math.min(
|
||||
Math.max((state.index || entries.length) - 1, 0),
|
||||
entries.length - 1
|
||||
);
|
||||
currentUrl = entries[index]?.url || null;
|
||||
}
|
||||
if (
|
||||
currentUrl === data.url ||
|
||||
(currentUrl && previousPinUrl && currentUrl !== previousPinUrl)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
state.entries = [
|
||||
{
|
||||
url: data.url,
|
||||
title: data.title || "",
|
||||
triggeringPrincipal_base64: lazy.E10SUtils.serializePrincipal(
|
||||
Services.scriptSecurityManager.createContentPrincipal(
|
||||
Services.io.newURI(data.url),
|
||||
{}
|
||||
)
|
||||
),
|
||||
},
|
||||
];
|
||||
state.index = 1;
|
||||
state.image = syncableIconUrl(data.icon) || undefined;
|
||||
delete state.scroll;
|
||||
win.SessionStore.setTabState(tab, state);
|
||||
} catch (e) {
|
||||
console.error("ZenSpacesSync: failed to retarget unloaded tab", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a tab or split group under the folder the record names, or back at
|
||||
* top level when it names none.
|
||||
*
|
||||
* @param {Window} win
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} item
|
||||
* @param {?string} folderId
|
||||
*/
|
||||
#applyFolderMembership(win, item, folderId) {
|
||||
const currentFolder = item.group?.isZenFolder ? item.group.id : null;
|
||||
const wantFolder = folderId || null;
|
||||
if (currentFolder === wantFolder) {
|
||||
return;
|
||||
}
|
||||
const target = wantFolder && this.#itemIn(win, wantFolder);
|
||||
if (target?.isZenFolder) {
|
||||
target.addTabs([item]);
|
||||
} else if (!wantFolder && currentFolder) {
|
||||
// ungroupTab pops a single nesting level. An item inside a subfolder
|
||||
// lands inside the parent folder, so keep going until it is actually
|
||||
// top-level.
|
||||
while (win.gBrowser.isTabGroup(item.group) && item.group.isZenFolder) {
|
||||
win.gBrowser.ungroupTab(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#updateTab(win, tab, data) {
|
||||
this.#updateTabIdentity(win, tab, data);
|
||||
|
||||
const isEssential = tab.hasAttribute("zen-essential");
|
||||
if (data.essential && !isEssential) {
|
||||
syncLog(`incoming record promotes tab ${tab.id} to essential`);
|
||||
win.gZenPinnedTabManager.addToEssentials(tab, { replicating: true });
|
||||
return;
|
||||
}
|
||||
if (!data.essential && isEssential) {
|
||||
console.warn(
|
||||
`ZenSpacesSync: incoming record demotes essential tab ${tab.id}`
|
||||
);
|
||||
win.gZenPinnedTabManager.removeEssentials(tab, /* unpin */ false);
|
||||
}
|
||||
if (data.essential) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inSplit = tab.group?.hasAttribute("split-view-group");
|
||||
if (inSplit) {
|
||||
// The split record governs placement of its members.
|
||||
return;
|
||||
}
|
||||
this.#applyFolderMembership(win, tab, data.folderId);
|
||||
if (
|
||||
!data.folderId &&
|
||||
data.workspaceUuid &&
|
||||
tab.getAttribute("zen-workspace-id") !== data.workspaceUuid
|
||||
) {
|
||||
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
||||
}
|
||||
}
|
||||
|
||||
#removeTab(win, tab) {
|
||||
if (tab.splitView) {
|
||||
win.gZenViewSplitter.removeTabFromGroup(tab, undefined, {
|
||||
forUnsplit: true,
|
||||
changeTab: false,
|
||||
});
|
||||
}
|
||||
win.gBrowser.removeTab(tab, { animate: true });
|
||||
}
|
||||
|
||||
#deleteTabs(win, removals, fail) {
|
||||
for (const { key: tabId, record } of removals) {
|
||||
try {
|
||||
const tab = this.#itemIn(win, tabId);
|
||||
if (win.gBrowser.isTab(tab)) {
|
||||
if (tab.hasAttribute("zen-essential")) {
|
||||
console.warn(
|
||||
`ZenSpacesSync: incoming tombstone removes essential tab ${tabId}`,
|
||||
tab.linkedBrowser?.currentURI?.spec ?? ""
|
||||
);
|
||||
} else {
|
||||
syncLog(`incoming tombstone removes tab ${tabId}`);
|
||||
}
|
||||
this.#removeTab(win, tab);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark: splits */
|
||||
|
||||
#applySplits(win, splits, fail) {
|
||||
for (const { key: splitId, data, record } of splits) {
|
||||
try {
|
||||
const members = (data.tabs || [])
|
||||
.map(id => this.#itemIn(win, id))
|
||||
.filter(tab => win.gBrowser.isTab(tab));
|
||||
const existing = this.#itemIn(win, splitId);
|
||||
const hasGroup = existing?.hasAttribute?.("split-view-group");
|
||||
if (hasGroup) {
|
||||
const wanted = new Set(data.tabs || []);
|
||||
for (const tab of [...existing.tabs]) {
|
||||
if (tab.id && !wanted.has(tab.id)) {
|
||||
win.gZenViewSplitter.removeTabFromGroup(tab, undefined, {
|
||||
changeTab: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (members.length >= 2) {
|
||||
win.gZenViewSplitter.splitTabs(members, data.gridType, -1, {
|
||||
groupFetchId: splitId,
|
||||
});
|
||||
}
|
||||
const group = this.#itemIn(win, splitId);
|
||||
if (group?.hasAttribute?.("split-view-group")) {
|
||||
this.#applyFolderMembership(win, group, data.folderId);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#deleteSplits(win, removals, fail) {
|
||||
for (const { key: splitId, record } of removals) {
|
||||
try {
|
||||
const index = win.gZenViewSplitter._data.findIndex(
|
||||
group => group.groupId === splitId
|
||||
);
|
||||
if (index >= 0) {
|
||||
win.gZenViewSplitter.removeGroup(index);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark: ordering */
|
||||
|
||||
#applyRelativeOrder(win, elements) {
|
||||
let prev = null;
|
||||
for (const el of elements) {
|
||||
if (!el) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
prev &&
|
||||
prev.parentNode &&
|
||||
prev.parentNode === el.parentNode &&
|
||||
prev.nextElementSibling !== el
|
||||
) {
|
||||
win.gBrowser.zenHandleTabMove(el, () => prev.after(el));
|
||||
}
|
||||
prev = el;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordering runs in the first synced window only: every move goes through
|
||||
* zenHandleTabMove, whose TabMove events window sync replicates into the
|
||||
* other windows.
|
||||
*
|
||||
* @param {Window} win
|
||||
* @param {object} incoming
|
||||
* @param {Function} fail
|
||||
*/
|
||||
#applyOrdering(win, incoming, fail) {
|
||||
for (const { key, data, record } of [
|
||||
...incoming.spaces,
|
||||
...incoming.folders,
|
||||
]) {
|
||||
try {
|
||||
if (Array.isArray(data.children) && data.children.length) {
|
||||
// A folder's empty-tab placeholder anchors its first child: without
|
||||
// it, relative ordering could never move anything to the start.
|
||||
const folder = data.folderId ? this.#itemIn(win, key) : null;
|
||||
const start = folder?.isZenFolder
|
||||
? folder.tabs.find(t => t.hasAttribute("zen-empty-tab"))
|
||||
: null;
|
||||
this.#applyRelativeOrder(win, [
|
||||
start ?? null,
|
||||
...data.children.map(id => this.#itemIn(win, id)),
|
||||
]);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
if (!incoming.layout) {
|
||||
return;
|
||||
}
|
||||
const { data, record } = incoming.layout;
|
||||
try {
|
||||
for (const tabIds of Object.values(data.essentials || {})) {
|
||||
if (Array.isArray(tabIds) && tabIds.length > 1) {
|
||||
this.#applyRelativeOrder(
|
||||
win,
|
||||
tabIds.map(id => this.#itemIn(win, id))
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.spaces) && data.spaces.length > 1) {
|
||||
const current = win.gZenWorkspaces.getWorkspaces();
|
||||
const byUuid = new Map(current.map(s => [s.uuid, s]));
|
||||
const ordered = data.spaces
|
||||
.map(uuid => byUuid.get(uuid))
|
||||
.filter(Boolean);
|
||||
for (const space of current) {
|
||||
if (!data.spaces.includes(space.uuid)) {
|
||||
ordered.push(space);
|
||||
}
|
||||
}
|
||||
const changedOrder = ordered.some(
|
||||
(space, i) => current[i]?.uuid !== space.uuid
|
||||
);
|
||||
if (changedOrder && ordered.length === current.length) {
|
||||
win.gZenWindowSync.propagateWorkspacesToAllWindows(ordered);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
fail(record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ZenSpacesSyncApplier = new nsZenSpacesSyncApplier();
|
||||
737
src/zen/sync/ZenSpacesSyncModel.sys.mjs
Normal file
737
src/zen/sync/ZenSpacesSyncModel.sys.mjs
Normal file
@@ -0,0 +1,737 @@
|
||||
/* 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, {
|
||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||
ZenLiveFoldersManager:
|
||||
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
||||
ContextualIdentityService:
|
||||
"moz-src:///toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"syncDebug",
|
||||
"zen.spaces-sync.debug",
|
||||
false
|
||||
);
|
||||
|
||||
/**
|
||||
* Debug logging for the whole Spaces sync pipeline.
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {...any} args
|
||||
*/
|
||||
export function syncLog(message, ...args) {
|
||||
if (lazy.syncDebug) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`ZenSpacesSync: ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export const SIDEBAR_COLLECTED_TOPIC = "zen-sidebar-data-collected";
|
||||
|
||||
export const RECORD_KINDS = Object.freeze({
|
||||
CONTAINER: "container",
|
||||
SPACE: "space",
|
||||
TAB: "tab",
|
||||
FOLDER: "folder",
|
||||
SPLIT: "split",
|
||||
LAYOUT: "layout",
|
||||
});
|
||||
|
||||
export const LAYOUT_RECORD_ID = "layout";
|
||||
|
||||
// Firefox ships four built-in containers with fixed userContextIds. They
|
||||
// exist on every device, so they map to well-known guids instead of the
|
||||
// per-profile generated ones.
|
||||
const BUILTIN_CONTAINER_MAX = 4;
|
||||
const BUILTIN_GUID_PREFIX = "builtin-";
|
||||
|
||||
const STORE_FILE_NAME = "zen-spaces-sync.json";
|
||||
const STORE_VERSION = 1;
|
||||
|
||||
// Everything setIcon accepts without a loading principal.
|
||||
const LOCAL_ICON_PROTOCOLS = ["data:", "chrome:", "about:", "resource:"];
|
||||
|
||||
/**
|
||||
* Normalizes a tab icon for syncing. The image attribute can hold a
|
||||
* moz-remote-image: wrapper around the actual (svg data:) favicon, carrying
|
||||
* a per-process contentParentId that means nothing on another device — sync
|
||||
* the wrapped URL instead. Anything else setIcon would reject as remote is
|
||||
* dropped.
|
||||
*
|
||||
* @param {?string} icon
|
||||
* @returns {string}
|
||||
*/
|
||||
export function syncableIconUrl(icon) {
|
||||
if (!icon || typeof icon !== "string") {
|
||||
return "";
|
||||
}
|
||||
if (icon.startsWith("moz-remote-image:")) {
|
||||
try {
|
||||
const uri = Services.io.newURI(icon);
|
||||
icon = new URLSearchParams(uri.query).get("url") || "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return LOCAL_ICON_PROTOCOLS.some(protocol => icon.startsWith(protocol))
|
||||
? icon
|
||||
: "";
|
||||
}
|
||||
|
||||
function sortedClone(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(sortedClone);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const out = {};
|
||||
for (const key of Object.keys(value).sort()) {
|
||||
out[key] = sortedClone(value[key]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic JSON serialization (recursively sorted keys), so that two
|
||||
* structurally equal payloads always stringify identically.
|
||||
*
|
||||
* @param {object} value
|
||||
*/
|
||||
export function canonicalJSON(value) {
|
||||
return JSON.stringify(sortedClone(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Digest of a record's canonical form. The uploaded-state snapshot only ever
|
||||
* needs equality checks, so it stores these short digests instead of the
|
||||
* full canonical strings, keeping the store file small and cheap to write.
|
||||
*
|
||||
* @param {string} kind
|
||||
* @param {object} data
|
||||
* @returns {string} base64 SHA-256 of the canonical record payload.
|
||||
*/
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export function recordDigest(kind, data) {
|
||||
const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
|
||||
Ci.nsICryptoHash
|
||||
);
|
||||
hasher.init(Ci.nsICryptoHash.SHA256);
|
||||
const bytes = textEncoder.encode(canonicalJSON({ kind, data }));
|
||||
hasher.update(bytes, bytes.length);
|
||||
return hasher.finish(/* base64 = */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the synced-data model for the Spaces engine.
|
||||
*/
|
||||
class nsZenSpacesSyncModel {
|
||||
#file = null;
|
||||
#cache = null;
|
||||
|
||||
#data() {
|
||||
if (!this.#file) {
|
||||
this.#file = new JSONFile({
|
||||
path: PathUtils.join(PathUtils.profileDir, STORE_FILE_NAME),
|
||||
dataPostProcessor: data => {
|
||||
if (data.version !== STORE_VERSION) {
|
||||
// Drop the uploaded snapshot so everything re-diffs against
|
||||
// the (also wiped) server, but keep the container identity mappings.
|
||||
data.uploaded = {};
|
||||
}
|
||||
data.version = STORE_VERSION;
|
||||
data.uploaded ||= {};
|
||||
data.containers ||= {};
|
||||
// The container map used to be keyed by guid. It is keyed by
|
||||
// userContextId now so that registering a guid replaces any
|
||||
// previous one for the same container. Flip old stores over
|
||||
// (old values are numeric ids, new values are guid strings).
|
||||
for (const [key, value] of Object.entries(data.containers)) {
|
||||
if (typeof value === "number") {
|
||||
delete data.containers[key];
|
||||
data.containers[value] = key;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
||||
this.#file.ensureDataReady();
|
||||
}
|
||||
return this.#file.data;
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.#cache = null;
|
||||
}
|
||||
|
||||
/* Mark: container identity */
|
||||
|
||||
guidForContextId(userContextId, { create = false } = {}) {
|
||||
const id = Number(userContextId);
|
||||
if (!Number.isSafeInteger(id) || id <= 0) {
|
||||
return null;
|
||||
}
|
||||
const data = this.#data();
|
||||
if (!lazy.ContextualIdentityService.getPublicIdentityFromId(id)) {
|
||||
// A space or tab still pointing at a container that was deleted.
|
||||
if (id in data.containers) {
|
||||
delete data.containers[id];
|
||||
this.#file.saveSoon();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (id <= BUILTIN_CONTAINER_MAX) {
|
||||
return `${BUILTIN_GUID_PREFIX}${id}`;
|
||||
}
|
||||
const existing = data.containers[id];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
const guid = Services.uuid.generateUUID().toString().slice(1, -1);
|
||||
data.containers[id] = guid;
|
||||
this.#file.saveSoon();
|
||||
return guid;
|
||||
}
|
||||
|
||||
contextIdForGuid(guid) {
|
||||
if (typeof guid !== "string" || !guid) {
|
||||
return null;
|
||||
}
|
||||
const data = this.#data();
|
||||
for (const [id, mapped] of Object.entries(data.containers)) {
|
||||
if (mapped === guid) {
|
||||
const contextId = Number(id);
|
||||
if (lazy.ContextualIdentityService.getPublicIdentityFromId(contextId)) {
|
||||
return contextId;
|
||||
}
|
||||
delete data.containers[id];
|
||||
this.#file.saveSoon();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (guid.startsWith(BUILTIN_GUID_PREFIX)) {
|
||||
const id = Number(guid.slice(BUILTIN_GUID_PREFIX.length));
|
||||
return Number.isSafeInteger(id) &&
|
||||
id > 0 &&
|
||||
id <= BUILTIN_CONTAINER_MAX &&
|
||||
lazy.ContextualIdentityService.getPublicIdentityFromId(id)
|
||||
? id
|
||||
: null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adopts an incoming guid as the identity's synced name.
|
||||
*
|
||||
* @param {string} guid
|
||||
* @param {number} userContextId
|
||||
*/
|
||||
registerContainerGuid(guid, userContextId) {
|
||||
this.#data().containers[userContextId] = guid;
|
||||
this.#file.saveSoon();
|
||||
}
|
||||
|
||||
forgetContainerGuid(guid) {
|
||||
const data = this.#data();
|
||||
for (const [id, mapped] of Object.entries(data.containers)) {
|
||||
if (mapped === guid) {
|
||||
delete data.containers[id];
|
||||
this.#file.saveSoon();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark: projections */
|
||||
|
||||
#isSyncableTab(tabData) {
|
||||
return !!(
|
||||
tabData &&
|
||||
tabData.zenSyncId &&
|
||||
(tabData.pinned || tabData.zenEssential) &&
|
||||
!tabData.zenIsEmpty &&
|
||||
!tabData.zenIsGlance &&
|
||||
!tabData.zenLiveFolderItemId
|
||||
);
|
||||
}
|
||||
|
||||
#tabIdentity(tabData) {
|
||||
const initial = tabData._zenPinnedInitialState;
|
||||
let url = initial?.entry?.url;
|
||||
let title = initial?.entry?.title;
|
||||
if (!url || url === "about:blank") {
|
||||
const entries = tabData.entries || [];
|
||||
if (entries.length) {
|
||||
let index = Math.min(
|
||||
Math.max((tabData.index || entries.length) - 1, 0),
|
||||
entries.length - 1
|
||||
);
|
||||
const entry = entries[index] || entries[0];
|
||||
if (entry?.url && entry.url !== "about:blank") {
|
||||
url = entry.url;
|
||||
title ??= entry.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!url || url === "about:blank") {
|
||||
return null;
|
||||
}
|
||||
const icon = syncableIconUrl(
|
||||
(tabData.zenHasStaticIcon
|
||||
? tabData.image
|
||||
: initial?.image || tabData.image) || ""
|
||||
);
|
||||
return { url, title: title || "", icon };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the ordered child id list for one parent (a space's pinned
|
||||
* section or a folder). Tabs and splits are ordered by their position in
|
||||
* the collected tab list; folders are spliced in next to their recorded
|
||||
* previous sibling.
|
||||
*
|
||||
* @param {object} root0
|
||||
* @param {Array<object>} root0.tabs
|
||||
* @param {Array<object>} root0.folders
|
||||
* @param {Set<string>} root0.splitIds
|
||||
* @param {Map<string, ?string>} root0.splitParents
|
||||
* @param {Map<string, ?string>} root0.splitWs
|
||||
* @param {object} root0.scope
|
||||
*/
|
||||
#childSequence({ tabs, folders, splitIds, splitParents, splitWs, scope }) {
|
||||
const seq = [];
|
||||
const seenSplits = new Set();
|
||||
for (const tab of tabs) {
|
||||
const groupId = tab.groupId || null;
|
||||
if (groupId && splitIds.has(groupId)) {
|
||||
if (
|
||||
!seenSplits.has(groupId) &&
|
||||
scope.matchSplit(
|
||||
splitParents.get(groupId) ?? null,
|
||||
splitWs.get(groupId) ?? null
|
||||
)
|
||||
) {
|
||||
seenSplits.add(groupId);
|
||||
seq.push(groupId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (scope.matchTab(tab, groupId)) {
|
||||
seq.push(tab.zenSyncId);
|
||||
}
|
||||
}
|
||||
for (const folder of folders) {
|
||||
if (!scope.matchFolder(folder)) {
|
||||
continue;
|
||||
}
|
||||
let index = seq.length;
|
||||
const prev = folder.prevSiblingInfo;
|
||||
if (!prev || prev.type === "start") {
|
||||
// No stored sibling means the folder is the first child of its
|
||||
// container (the section rebuild on startup places content before
|
||||
// the fake start element, so the first item has no sibling at all).
|
||||
index = 0;
|
||||
} else if (prev.id) {
|
||||
const at = seq.indexOf(prev.id);
|
||||
if (at !== -1) {
|
||||
index = at + 1;
|
||||
} else if (prev.type === "tab") {
|
||||
// An unsynced tab sibling (empty placeholders, live folder items)
|
||||
// always sits at the start of its container.
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
seq.splice(index, 0, folder.id);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the shared projection context from one sidebar collection.
|
||||
* Syncable tabs, folders, the syncable splits and their parents and
|
||||
* workspaces, plus the folder-membership resolver.
|
||||
*
|
||||
* @param {object} sidebar
|
||||
*/
|
||||
#projectionContext(sidebar) {
|
||||
const tabs = (sidebar.tabs || []).filter(t => this.#isSyncableTab(t));
|
||||
const folders = (sidebar.folders || []).filter(
|
||||
f => f?.id && !f.splitViewGroup
|
||||
);
|
||||
const folderIds = new Set(folders.map(f => f.id));
|
||||
const allSplits = (sidebar.splitViewData || []).filter(g => g?.groupId);
|
||||
const splitParents = new Map();
|
||||
for (const entry of sidebar.folders || []) {
|
||||
if (entry?.splitViewGroup && entry.id) {
|
||||
splitParents.set(entry.id, entry.parentId || null);
|
||||
}
|
||||
}
|
||||
|
||||
// Only splits whose members are all synced tabs are synced themselves.
|
||||
const syncableTabIds = new Set(tabs.map(t => t.zenSyncId));
|
||||
const splits = allSplits.filter(
|
||||
g =>
|
||||
Array.isArray(g.tabs) &&
|
||||
g.tabs.length >= 2 &&
|
||||
g.tabs.every(id => syncableTabIds.has(id))
|
||||
);
|
||||
const splitIds = new Set(splits.map(g => g.groupId));
|
||||
const splitWs = new Map();
|
||||
for (const tab of tabs) {
|
||||
const groupId = tab.groupId || null;
|
||||
if (groupId && splitIds.has(groupId) && !splitWs.has(groupId)) {
|
||||
splitWs.set(groupId, tab.zenWorkspace || null);
|
||||
}
|
||||
}
|
||||
const folderOf = groupId => {
|
||||
if (!groupId) {
|
||||
return null;
|
||||
}
|
||||
if (splitIds.has(groupId)) {
|
||||
return splitParents.get(groupId) || null;
|
||||
}
|
||||
return folderIds.has(groupId) ? groupId : null;
|
||||
};
|
||||
return { tabs, folders, splits, splitIds, splitParents, splitWs, folderOf };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map} map
|
||||
* @param {object} ctx - The projection context.
|
||||
*/
|
||||
#projectTabs(map, ctx) {
|
||||
for (const tab of ctx.tabs) {
|
||||
const identity = this.#tabIdentity(tab);
|
||||
if (!identity) {
|
||||
continue;
|
||||
}
|
||||
const essential = !!tab.zenEssential;
|
||||
map.set(tab.zenSyncId, {
|
||||
kind: RECORD_KINDS.TAB,
|
||||
data: {
|
||||
tabId: tab.zenSyncId,
|
||||
url: identity.url,
|
||||
title: identity.title,
|
||||
icon: identity.icon,
|
||||
containerGuid: this.guidForContextId(tab.userContextId, {
|
||||
create: true,
|
||||
}),
|
||||
essential,
|
||||
workspaceUuid: essential ? null : tab.zenWorkspace || null,
|
||||
folderId: ctx.folderOf(tab.groupId || null),
|
||||
staticLabel:
|
||||
typeof tab.zenStaticLabel === "string" ? tab.zenStaticLabel : null,
|
||||
hasStaticIcon: !!tab.zenHasStaticIcon,
|
||||
defaultContainer: !!tab.zenDefaultUserContextId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
projections() {
|
||||
const sidebar = lazy.ZenSessionStore.getSidebarData() || {};
|
||||
const stamp = sidebar.lastCollected || 0;
|
||||
if (this.#cache && this.#cache.stamp === stamp) {
|
||||
return this.#cache.map;
|
||||
}
|
||||
|
||||
const map = new Map();
|
||||
const pending = new Set();
|
||||
const ctx = this.#projectionContext(sidebar);
|
||||
const { tabs, folders, splits, splitIds, splitParents, splitWs } = ctx;
|
||||
|
||||
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
|
||||
if (!identity.name) {
|
||||
continue;
|
||||
}
|
||||
const guid = this.guidForContextId(identity.userContextId, {
|
||||
create: true,
|
||||
});
|
||||
if (!guid) {
|
||||
continue;
|
||||
}
|
||||
map.set(guid, {
|
||||
kind: RECORD_KINDS.CONTAINER,
|
||||
data: {
|
||||
guid,
|
||||
name: identity.name,
|
||||
icon: identity.icon || "",
|
||||
color: identity.color || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const spaces = sidebar.spaces || [];
|
||||
for (const space of spaces) {
|
||||
if (!space?.uuid) {
|
||||
continue;
|
||||
}
|
||||
const uuid = space.uuid;
|
||||
map.set(uuid, {
|
||||
kind: RECORD_KINDS.SPACE,
|
||||
data: {
|
||||
uuid,
|
||||
name: space.name ?? "",
|
||||
icon: space.icon ?? null,
|
||||
theme: space.theme ?? null,
|
||||
containerGuid: this.guidForContextId(space.containerTabId, {
|
||||
create: true,
|
||||
}),
|
||||
children: this.#childSequence({
|
||||
tabs,
|
||||
folders,
|
||||
splitIds,
|
||||
splitParents,
|
||||
splitWs,
|
||||
scope: {
|
||||
matchTab: (t, groupId) =>
|
||||
!groupId &&
|
||||
!t.zenEssential &&
|
||||
(t.zenWorkspace || null) === uuid,
|
||||
matchSplit: (parent, ws) => !parent && ws === uuid,
|
||||
matchFolder: f => !f.parentId && (f.workspaceId || null) === uuid,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const folder of folders) {
|
||||
const fid = folder.id;
|
||||
let live = null;
|
||||
if (folder.isLiveFolder) {
|
||||
try {
|
||||
live = lazy.ZenLiveFoldersManager.getSyncableFolderData(fid);
|
||||
} catch (e) {
|
||||
console.error("ZenSpacesSync: failed to project live folder", e);
|
||||
}
|
||||
if (!live) {
|
||||
pending.add(fid);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
map.set(fid, {
|
||||
kind: RECORD_KINDS.FOLDER,
|
||||
data: {
|
||||
folderId: fid,
|
||||
name: folder.name ?? "",
|
||||
icon: folder.userIcon || null,
|
||||
workspaceUuid: folder.workspaceId || null,
|
||||
parentFolderId: folder.parentId || null,
|
||||
live,
|
||||
children: this.#childSequence({
|
||||
tabs,
|
||||
folders,
|
||||
splitIds,
|
||||
splitParents,
|
||||
splitWs,
|
||||
scope: {
|
||||
matchTab: (t, groupId) => groupId === fid,
|
||||
matchSplit: parent => parent === fid,
|
||||
matchFolder: f => f.parentId === fid,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.#projectTabs(map, ctx);
|
||||
|
||||
for (const split of splits) {
|
||||
map.set(split.groupId, {
|
||||
kind: RECORD_KINDS.SPLIT,
|
||||
data: {
|
||||
splitId: split.groupId,
|
||||
gridType: split.gridType || "grid",
|
||||
tabs: [...split.tabs],
|
||||
workspaceUuid: splitWs.get(split.groupId) ?? null,
|
||||
folderId: splitParents.get(split.groupId) || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (spaces.length) {
|
||||
const essentials = {};
|
||||
for (const tab of tabs) {
|
||||
if (!tab.zenEssential) {
|
||||
continue;
|
||||
}
|
||||
const key =
|
||||
this.guidForContextId(tab.userContextId, { create: true }) ||
|
||||
"default";
|
||||
(essentials[key] ||= []).push(tab.zenSyncId);
|
||||
}
|
||||
map.set(LAYOUT_RECORD_ID, {
|
||||
kind: RECORD_KINDS.LAYOUT,
|
||||
data: {
|
||||
spaces: spaces.map(s => s.uuid).filter(Boolean),
|
||||
essentials,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.#cache = { stamp, map, pending };
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids that are locally present but deliberately not projected this cycle
|
||||
* (live folders whose provider config isn't available yet). The diff must
|
||||
* not read their absence as a deletion.
|
||||
*/
|
||||
#pendingIds() {
|
||||
this.projections();
|
||||
return this.#cache.pending;
|
||||
}
|
||||
|
||||
#digestCache = null;
|
||||
|
||||
/**
|
||||
* Digests of all current projections. Change detection runs several times per
|
||||
* sync cycle (tracker observe, changed-id computation, upload bookkeeping)
|
||||
* and only needs to hash again after a fresh sidebar collection.
|
||||
*/
|
||||
#digestAll() {
|
||||
const map = this.projections();
|
||||
if (this.#digestCache?.map === map) {
|
||||
return this.#digestCache.digests;
|
||||
}
|
||||
const digests = new Map();
|
||||
for (const [id, projected] of map) {
|
||||
digests.set(id, recordDigest(projected.kind, projected.data));
|
||||
}
|
||||
this.#digestCache = { map, digests };
|
||||
return digests;
|
||||
}
|
||||
|
||||
/* Mark: engine-facing API */
|
||||
|
||||
getAllRecordIds() {
|
||||
const ids = {};
|
||||
for (const id of this.projections().keys()) {
|
||||
ids[id] = true;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
itemExists(id) {
|
||||
return this.projections().has(id);
|
||||
}
|
||||
|
||||
projectRecord(id) {
|
||||
return this.projections().get(id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes = diff between the current projections and the last state the
|
||||
* server acknowledged. Ids present locally with different content are
|
||||
* modified; ids only present in the uploaded snapshot are deletions.
|
||||
*/
|
||||
computeChangedIDs() {
|
||||
const uploaded = this.#data().uploaded;
|
||||
const current = this.#digestAll();
|
||||
const pending = this.#pendingIds();
|
||||
const now = Date.now() / 1000;
|
||||
const changes = {};
|
||||
for (const [id, digest] of current) {
|
||||
if (uploaded[id] !== digest) {
|
||||
changes[id] = now;
|
||||
}
|
||||
}
|
||||
for (const id of Object.keys(uploaded)) {
|
||||
if (!current.has(id) && !pending.has(id)) {
|
||||
changes[id] = now;
|
||||
}
|
||||
}
|
||||
if (lazy.syncDebug && Object.keys(changes).length) {
|
||||
const map = this.projections();
|
||||
syncLog(
|
||||
"outgoing diff:",
|
||||
Object.keys(changes).map(id =>
|
||||
current.has(id) ? `${map.get(id)?.kind} ${id}` : `TOMBSTONE ${id}`
|
||||
)
|
||||
);
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
hasPendingChanges() {
|
||||
const uploaded = this.#data().uploaded;
|
||||
const current = this.#digestAll();
|
||||
const pending = this.#pendingIds();
|
||||
for (const [id, digest] of current) {
|
||||
if (uploaded[id] !== digest) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const id of Object.keys(uploaded)) {
|
||||
if (!current.has(id) && !pending.has(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after records were successfully uploaded. Remember exactly what
|
||||
* the server now holds for them.
|
||||
*
|
||||
* @param {Array<string>} ids
|
||||
*/
|
||||
markUploaded(ids) {
|
||||
const data = this.#data();
|
||||
const current = this.#digestAll();
|
||||
for (const id of ids) {
|
||||
if (current.has(id)) {
|
||||
data.uploaded[id] = current.get(id);
|
||||
} else {
|
||||
delete data.uploaded[id];
|
||||
}
|
||||
}
|
||||
if (lazy.syncDebug && ids.length) {
|
||||
syncLog(
|
||||
"server acknowledged upload:",
|
||||
ids.map(id => (current.has(id) ? id : `${id} (tombstone)`))
|
||||
);
|
||||
}
|
||||
this.#file.saveSoon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after an incoming record was applied locally. Storing the
|
||||
* incoming payload as the uploaded state means a faithful local
|
||||
* materialization produces no re-upload, while a divergent one re-uploads
|
||||
* the local truth (self-healing).
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {?object} cleartext - null for tombstones.
|
||||
*/
|
||||
noteApplied(id, cleartext) {
|
||||
const data = this.#data();
|
||||
if (!cleartext) {
|
||||
delete data.uploaded[id];
|
||||
} else {
|
||||
data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data);
|
||||
}
|
||||
syncLog(
|
||||
`acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}`
|
||||
);
|
||||
this.#file.saveSoon();
|
||||
}
|
||||
}
|
||||
|
||||
export const ZenSpacesSyncModel = new nsZenSpacesSyncModel();
|
||||
@@ -1,160 +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/. */
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||
ContextualIdentityService:
|
||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
||||
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
||||
});
|
||||
|
||||
function normalizeUserContextId(value) {
|
||||
const normalized = typeof value === "string" ? Number(value) : value;
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
class ZenSyncManager {
|
||||
getSidebarData() {
|
||||
return lazy.ZenSessionStore.getSidebarData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to ignore changes to items. This is used to prevent
|
||||
* infinite loops when applying incoming sync changes.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#ignoreChanges = false;
|
||||
|
||||
#changedItems = new Map();
|
||||
|
||||
markItemChanged(item) {
|
||||
if (item.type && item.id && !this.#ignoreChanges) {
|
||||
const key = `${item.type}~${item.id}`;
|
||||
this.#changedItems.set(key, { type: item.type, id: item.id });
|
||||
}
|
||||
}
|
||||
|
||||
#getChangedItems() {
|
||||
return Array.from(this.#changedItems.values());
|
||||
}
|
||||
|
||||
#clearChangedItems() {
|
||||
this.#changedItems.clear();
|
||||
}
|
||||
|
||||
notifyAboutChanges() {
|
||||
const changedItems = this.#getChangedItems();
|
||||
|
||||
for (const item of changedItems) {
|
||||
Services.obs.notifyObservers(
|
||||
{ wrappedJSObject: item },
|
||||
"zen-workspace-item-changed"
|
||||
);
|
||||
}
|
||||
this.#clearChangedItems();
|
||||
}
|
||||
|
||||
async applyIncomingBatch(pulled, removals) {
|
||||
try {
|
||||
this.#ignoreChanges = true;
|
||||
this.#applyIncomingContainers(
|
||||
pulled.containers || [],
|
||||
removals.containers || []
|
||||
);
|
||||
|
||||
const win = lazy.ZenWindowSync.firstSyncedWindow;
|
||||
if (win?.gZenWorkspaces) {
|
||||
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.#ignoreChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
#applyIncomingContainers(pulledContainers, removedContainers) {
|
||||
const localContainersById = new Map(
|
||||
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
|
||||
container.userContextId,
|
||||
container,
|
||||
])
|
||||
);
|
||||
|
||||
for (const container of pulledContainers) {
|
||||
if (!container.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const userContextId = normalizeUserContextId(container.userContextId);
|
||||
if (userContextId === null) {
|
||||
console.warn(
|
||||
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
|
||||
{ container }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existsLocally = localContainersById.has(userContextId);
|
||||
|
||||
if (existsLocally) {
|
||||
lazy.ContextualIdentityService.update(
|
||||
userContextId,
|
||||
container.name,
|
||||
container.icon,
|
||||
container.color
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdIdentity = lazy.ContextualIdentityService.create(
|
||||
container.name,
|
||||
container.icon,
|
||||
container.color,
|
||||
userContextId
|
||||
);
|
||||
if (createdIdentity) {
|
||||
localContainersById.set(createdIdentity.userContextId, createdIdentity);
|
||||
}
|
||||
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
|
||||
console.warn("ZenSyncManager: Container sync created unexpected ID", {
|
||||
requestedId: userContextId,
|
||||
createdId: createdIdentity.userContextId,
|
||||
name: container.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const container of removedContainers) {
|
||||
const userContextId = normalizeUserContextId(container.userContextId);
|
||||
if (userContextId === null) {
|
||||
console.warn(
|
||||
"ZenSyncManager: Ignoring container removal with invalid userContextId",
|
||||
{ container }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!localContainersById.has(userContextId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
lazy.ContextualIdentityService.remove(userContextId);
|
||||
localContainersById.delete(userContextId);
|
||||
} catch {
|
||||
// Container may already be gone locally.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ZenSyncStore = new ZenSyncManager();
|
||||
@@ -1,404 +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 {
|
||||
Store,
|
||||
SyncEngine,
|
||||
Tracker,
|
||||
} from "resource://services-sync/engines.sys.mjs";
|
||||
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
|
||||
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
||||
ContextualIdentityService:
|
||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
||||
});
|
||||
|
||||
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
|
||||
space: "s",
|
||||
container: "c",
|
||||
});
|
||||
|
||||
const RECORD_TYPE_BY_PREFIX = Object.freeze({
|
||||
s: "space",
|
||||
c: "container",
|
||||
});
|
||||
|
||||
/**
|
||||
* Sync record wrapper for workspace and container items stored in the
|
||||
* Workspaces engine collection.
|
||||
*/
|
||||
export class ZenWorkspacesRecord extends CryptoWrapper {
|
||||
_logName = "Sync.Record.ZenSpaces";
|
||||
}
|
||||
|
||||
ZenWorkspacesRecord.prototype.type = "spaces";
|
||||
|
||||
function parseRecordId(id) {
|
||||
const sep = id.indexOf("~");
|
||||
if (sep === -1) {
|
||||
return null;
|
||||
}
|
||||
const prefix = id.slice(0, sep);
|
||||
const key = id.slice(sep + 1);
|
||||
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
|
||||
}
|
||||
|
||||
function createRecordId(type, id) {
|
||||
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
|
||||
if (!prefix) {
|
||||
throw new Error(`Unknown Spaces Sync record type: ${type}`);
|
||||
}
|
||||
return `${prefix}~${id}`;
|
||||
}
|
||||
|
||||
function normalizeUserContextId(value) {
|
||||
const normalized = typeof value === "string" ? Number(value) : value;
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
|
||||
* and restores the item's real identity key where needed
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
function stripSyncFields(data) {
|
||||
const rest = { ...data };
|
||||
delete rest.id;
|
||||
delete rest.type;
|
||||
return rest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync store implementation that serializes local workspace and container
|
||||
* state into records and applies incoming remote changes.
|
||||
*/
|
||||
class ZenWorkspacesStore extends Store {
|
||||
constructor(name, engine) {
|
||||
super(name, engine);
|
||||
}
|
||||
|
||||
async getAllIDs() {
|
||||
const ids = {};
|
||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
||||
|
||||
for (const space of sidebar.spaces || []) {
|
||||
if (space.uuid) {
|
||||
ids[createRecordId("space", space.uuid)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
|
||||
ids[createRecordId("container", c.userContextId)] = true;
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async itemExists(id) {
|
||||
const parsed = parseRecordId(id);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
||||
|
||||
switch (parsed.type) {
|
||||
case "space":
|
||||
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
|
||||
case "container":
|
||||
return lazy.ContextualIdentityService.getPublicIdentities().some(
|
||||
c => String(c.userContextId) === parsed.key
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createRecord(id, collection) {
|
||||
const record = new ZenWorkspacesRecord(collection, id);
|
||||
const parsed = parseRecordId(id);
|
||||
if (!parsed) {
|
||||
record.deleted = true;
|
||||
return record;
|
||||
}
|
||||
|
||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
||||
|
||||
switch (parsed.type) {
|
||||
case "space": {
|
||||
const spaces = sidebar.spaces || [];
|
||||
const idx = spaces.findIndex(s => s.uuid === parsed.key);
|
||||
if (idx === -1) {
|
||||
record.deleted = true;
|
||||
return record;
|
||||
}
|
||||
const rest = { ...spaces[idx] };
|
||||
delete rest.syncStatus;
|
||||
record.cleartext = { id, type: "space", ...rest, position: idx };
|
||||
break;
|
||||
}
|
||||
|
||||
case "container": {
|
||||
const container =
|
||||
lazy.ContextualIdentityService.getPublicIdentities().find(
|
||||
c => String(c.userContextId) === parsed.key
|
||||
);
|
||||
if (!container) {
|
||||
record.deleted = true;
|
||||
return record;
|
||||
}
|
||||
record.cleartext = {
|
||||
id,
|
||||
type: "container",
|
||||
userContextId: container.userContextId,
|
||||
name: container.name,
|
||||
icon: container.icon,
|
||||
color: container.color,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
record.deleted = true;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async applyIncomingBatch(records, _countTelemetry) {
|
||||
const pulled = { spaces: [], containers: [] };
|
||||
const removals = { spaces: [], containers: [] };
|
||||
|
||||
for (const record of records) {
|
||||
if (record.deleted) {
|
||||
this._collectRemoval(record.id, removals);
|
||||
continue;
|
||||
}
|
||||
const data = record.cleartext;
|
||||
if (!data?.type) {
|
||||
continue;
|
||||
}
|
||||
const clean = stripSyncFields(data);
|
||||
switch (data.type) {
|
||||
case "space":
|
||||
pulled.spaces.push(clean);
|
||||
break;
|
||||
case "container":
|
||||
pulled.containers.push(clean);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress change tracking while applying incoming data to prevent
|
||||
// feedback loops where applied items get re-uploaded immediately.
|
||||
this.engine._tracker.ignoreAll = true;
|
||||
try {
|
||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
|
||||
} finally {
|
||||
this.engine._tracker.ignoreAll = false;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
_collectRemoval(id, removals) {
|
||||
const parsed = parseRecordId(id);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
switch (parsed.type) {
|
||||
case "space":
|
||||
removals.spaces.push({ uuid: parsed.key });
|
||||
break;
|
||||
case "container": {
|
||||
const userContextId = normalizeUserContextId(parsed.key);
|
||||
if (userContextId === null) {
|
||||
console.warn(
|
||||
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
|
||||
{ id }
|
||||
);
|
||||
break;
|
||||
}
|
||||
removals.containers.push({ userContextId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(record) {
|
||||
await this._applySingle(record);
|
||||
}
|
||||
|
||||
async update(record) {
|
||||
await this._applySingle(record);
|
||||
}
|
||||
|
||||
async _applySingle(record) {
|
||||
this.engine._tracker.ignoreAll = true;
|
||||
try {
|
||||
if (record.deleted) {
|
||||
const removals = { spaces: [], containers: [] };
|
||||
this._collectRemoval(record.id, removals);
|
||||
await lazy.ZenSyncStore.applyIncomingBatch(
|
||||
{ spaces: [], containers: [] },
|
||||
removals
|
||||
);
|
||||
return;
|
||||
}
|
||||
const data = record.cleartext;
|
||||
if (!data?.type) {
|
||||
return;
|
||||
}
|
||||
const clean = stripSyncFields(data);
|
||||
const pulled = { spaces: [], containers: [] };
|
||||
switch (data.type) {
|
||||
case "space":
|
||||
pulled.spaces.push(clean);
|
||||
break;
|
||||
case "container":
|
||||
pulled.containers.push(clean);
|
||||
break;
|
||||
}
|
||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
|
||||
spaces: [],
|
||||
containers: [],
|
||||
});
|
||||
} finally {
|
||||
this.engine._tracker.ignoreAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async remove() {
|
||||
// No-op: never delete user data on wipe
|
||||
}
|
||||
|
||||
async wipe() {
|
||||
// No-op: never delete user data on wipe
|
||||
}
|
||||
|
||||
changeItemID() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync tracker that watches workspace and contextual identity observers and
|
||||
* marks the corresponding record IDs as changed.
|
||||
*/
|
||||
class ZenWorkspacesTracker extends Tracker {
|
||||
#changedIDs = {};
|
||||
#ignoreAll = false;
|
||||
|
||||
get ignoreAll() {
|
||||
return this.#ignoreAll;
|
||||
}
|
||||
|
||||
set ignoreAll(value) {
|
||||
this.#ignoreAll = value;
|
||||
}
|
||||
|
||||
onStart() {
|
||||
Services.obs.addObserver(this, "zen-workspace-item-changed");
|
||||
Services.obs.addObserver(this, "contextual-identity-created");
|
||||
Services.obs.addObserver(this, "contextual-identity-updated");
|
||||
Services.obs.addObserver(this, "contextual-identity-deleted");
|
||||
}
|
||||
|
||||
onStop() {
|
||||
Services.obs.removeObserver(this, "zen-workspace-item-changed");
|
||||
Services.obs.removeObserver(this, "contextual-identity-created");
|
||||
Services.obs.removeObserver(this, "contextual-identity-updated");
|
||||
Services.obs.removeObserver(this, "contextual-identity-deleted");
|
||||
}
|
||||
|
||||
observe(subject, topic, _data) {
|
||||
if (this.#ignoreAll) {
|
||||
return;
|
||||
}
|
||||
if (topic === "zen-workspace-item-changed") {
|
||||
const type = subject?.wrappedJSObject?.type;
|
||||
const id = subject?.wrappedJSObject?.id;
|
||||
if (type && id) {
|
||||
this._trackChange({ type, id });
|
||||
}
|
||||
} else if (topic.startsWith("contextual-identity-")) {
|
||||
const id = subject?.wrappedJSObject?.userContextId;
|
||||
if (id && normalizeUserContextId(id) !== null) {
|
||||
this._trackChange({ type: "container", id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_trackChange(data) {
|
||||
if (data.type && data.id) {
|
||||
const id = createRecordId(data.type, data.id);
|
||||
this.#changedIDs[id] = Date.now() / 1000;
|
||||
this.score += SCORE_INCREMENT_XLARGE;
|
||||
}
|
||||
}
|
||||
|
||||
async getChangedIDs() {
|
||||
return { ...this.#changedIDs };
|
||||
}
|
||||
|
||||
async addChangedID(id, when) {
|
||||
this.#changedIDs[id] = when;
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeChangedID(...ids) {
|
||||
for (const id of ids) {
|
||||
delete this.#changedIDs[id];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
clearChangedIDs() {
|
||||
this.#changedIDs = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
|
||||
* implementations into Firefox Sync.
|
||||
*/
|
||||
export class ZenWorkspacesEngine extends SyncEngine {
|
||||
static get name() {
|
||||
return "Spaces";
|
||||
}
|
||||
|
||||
constructor(service) {
|
||||
super("Spaces", service);
|
||||
}
|
||||
|
||||
get _storeObj() {
|
||||
return ZenWorkspacesStore;
|
||||
}
|
||||
|
||||
get _trackerObj() {
|
||||
return ZenWorkspacesTracker;
|
||||
}
|
||||
|
||||
get _recordObj() {
|
||||
return ZenWorkspacesRecord;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
get syncPriority() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
get allowSkippedRecord() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXTRA_JS_MODULES.zen += [
|
||||
"ZenSyncManager.sys.mjs",
|
||||
"ZenWorkspacesSync.sys.mjs",
|
||||
"ZenSpacesSync.sys.mjs",
|
||||
"ZenSpacesSyncApplier.sys.mjs",
|
||||
"ZenSpacesSyncModel.sys.mjs",
|
||||
]
|
||||
|
||||
@@ -503,7 +503,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
}
|
||||
|
||||
addToEssentials(tab) {
|
||||
addToEssentials(tab, { replicating = false } = {}) {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const tabs = tab
|
||||
? // if it's already an array, dont make it [tab]
|
||||
@@ -518,7 +518,12 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
||||
// eslint-disable-next-line no-shadow
|
||||
let tab = tabs[i];
|
||||
const section = gZenWorkspaces.getEssentialsSection(tab);
|
||||
if (!this.canEssentialBeAdded(tab)) {
|
||||
// canEssentialBeAdded gates user-initiated adds on the *active* space's
|
||||
// container. A replicated add mirrors a decision already made in another
|
||||
// window or on another device, always into the tab's own container
|
||||
// section.
|
||||
if (!replicating && !this.canEssentialBeAdded(tab)) {
|
||||
this.log(`addToEssentials rejected for ${tab.id}`);
|
||||
movedAll = false;
|
||||
continue;
|
||||
}
|
||||
@@ -570,6 +575,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const tab = tabs[i];
|
||||
if (this._canLog) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(
|
||||
`ZenPinnedTabManager: removing tab ${tab.id} from essentials ` +
|
||||
`(unpin=${unpin})`
|
||||
);
|
||||
}
|
||||
tab.removeAttribute("zen-essential");
|
||||
if (
|
||||
gZenWorkspaces.workspaceEnabled &&
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
&:not([zen-compact-animating])
|
||||
#zen-sidebar-top-buttons:not(:has(#zen-sidebar-top-buttons-customization-target > *:not(#zen-sidebar-top-buttons-separator), .titlebar-buttonbox-container)) {
|
||||
max-height: 0 !important;
|
||||
|
||||
@@ -100,9 +100,10 @@
|
||||
width: -moz-available !important;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
& #urlbar-container[breakout="true"]:has(#urlbar[zen-floating-urlbar="true"]) {
|
||||
--urlbar-container-height: 36px !important;
|
||||
--urlbar-height: 38px !important;
|
||||
--urlbar-height: 36px !important;
|
||||
}
|
||||
|
||||
& #nav-bar {
|
||||
@@ -225,19 +226,9 @@
|
||||
========================================================================== */
|
||||
#navigator-toolbox {
|
||||
/* Define theme variables, including platform specifics for native look */
|
||||
--border-radius-medium: 10px;
|
||||
--tab-border-radius: 6px;
|
||||
--zen-toolbox-min-width: 1px;
|
||||
|
||||
@media (-moz-platform: windows) {
|
||||
--border-radius-medium: 6px;
|
||||
--tab-border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
--border-radius-medium: 12px;
|
||||
--tab-border-radius: 8px;
|
||||
}
|
||||
--border-radius-medium: 14px;
|
||||
--tab-border-radius: 8px;
|
||||
|
||||
min-width: var(--zen-toolbox-min-width);
|
||||
margin-top: 0 !important;
|
||||
@@ -329,6 +320,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-has-selector */
|
||||
:root:not([zen-renaming-tab="true"])
|
||||
#tabbrowser-tabs:not([movingtab])
|
||||
&:active:not(:has(.tab-content > image:active)) {
|
||||
@@ -538,8 +530,7 @@
|
||||
|
||||
:root[zen-single-toolbar="true"]:not([zen-has-empty-tab="true"]) & {
|
||||
& #urlbar:not([breakout-extend]) .urlbar-input-container {
|
||||
padding-left: 8px;
|
||||
padding-right: 4px;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1209,6 +1200,8 @@
|
||||
background-position: center;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: superellipse(var(--zen-squircle-value));
|
||||
}
|
||||
|
||||
background: transparent;
|
||||
@@ -1233,6 +1226,8 @@
|
||||
z-index: 0;
|
||||
content: "";
|
||||
transition: background 0.1s ease-in-out;
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: superellipse(var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1341,7 +1336,7 @@
|
||||
|
||||
#tabs-newtab-button:not([in-urlbar="true"]) label,
|
||||
.zen-current-workspace-indicator-name {
|
||||
opacity: 0.7;
|
||||
color: color-mix(in srgb, currentColor 70%, transparent);
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
# 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/.
|
||||
|
||||
[alerts]
|
||||
source = "browser/base/content/test/alerts"
|
||||
is_direct_path = true
|
||||
|
||||
[backforward]
|
||||
source = "browser/base/content/test/backforward"
|
||||
is_direct_path = true
|
||||
|
||||
[caps]
|
||||
source = "browser/base/content/test/caps"
|
||||
is_direct_path = true
|
||||
|
||||
[gesture]
|
||||
source = "browser/base/content/test/gesture"
|
||||
is_direct_path = true
|
||||
|
||||
[gesture.replace-manifest]
|
||||
"[DEFAULT]" = '''[DEFAULT]
|
||||
prefs = [
|
||||
"zen.window-sync.enabled=false",
|
||||
]'''
|
||||
|
||||
[linkHandling]
|
||||
source = "browser/base/content/test/linkHandling"
|
||||
is_direct_path = true
|
||||
disable = [
|
||||
# Glance opens with alt+click, firefox doesnt know that.
|
||||
"browser_javascript_links.js",
|
||||
]
|
||||
|
||||
[readermode]
|
||||
source = "toolkit/components/reader/tests/browser"
|
||||
is_direct_path = true
|
||||
@@ -13,10 +43,88 @@ disable = [
|
||||
"browser_readerMode_colorSchemePref.js",
|
||||
]
|
||||
|
||||
[remote-settings]
|
||||
source = "services/settings/test/unit"
|
||||
is_direct_path = true
|
||||
xpcshell = true
|
||||
|
||||
[remote-settings.replace-manifest]
|
||||
"../../../" = "../../../../services/"
|
||||
|
||||
[safebrowsing]
|
||||
source = "browser/components/safebrowsing/content/test"
|
||||
is_direct_path = true
|
||||
|
||||
[services-crypto]
|
||||
source = "services/crypto/tests/unit"
|
||||
is_direct_path = true
|
||||
xpcshell = true
|
||||
|
||||
[services-crypto.replace-manifest]
|
||||
"../../../" = "../../../../services/"
|
||||
|
||||
[sessionstore]
|
||||
source = "browser/components/sessionstore/test"
|
||||
is_direct_path = true
|
||||
disable = [
|
||||
"browser_1933485_tab_groups_history.js",
|
||||
"browser_1953801_tab_groups_history_close_other.js",
|
||||
"browser_aboutSessionRestore.js",
|
||||
"browser_bfcache_telemetry.js",
|
||||
"browser_closedId.js",
|
||||
"browser_closed_objects_changed_notifications_tabs.js",
|
||||
"browser_closed_objects_changed_notifications_windows.js",
|
||||
"browser_closed_tabs_closed_windows.js",
|
||||
"browser_closed_tabs_windows.js",
|
||||
"browser_cookies_partitioned.js",
|
||||
"browser_crashedTabs.js",
|
||||
"browser_dying_cache.js",
|
||||
"browser_firefoxView_restore.js",
|
||||
"browser_firefoxView_selected_restore.js",
|
||||
"browser_forget_closed_tab_window_byId.js",
|
||||
"browser_movePendingTabToNewWindow.js",
|
||||
"browser_newtab_userTypedValue.js",
|
||||
"browser_pinned_tabs.js",
|
||||
"browser_purge_domaindata.js",
|
||||
"browser_reopen_all_windows.js",
|
||||
"browser_restoreLastActionCorrectOrder.js",
|
||||
"browser_restoreLastClosedTabOrWindowOrSession.js",
|
||||
"browser_restore_container_tabs_oa.js",
|
||||
"browser_restore_tabless_window.js",
|
||||
"browser_restore_verticalPinnedTabs.js",
|
||||
"browser_restored_window_features.js",
|
||||
"browser_scrollPositions.js",
|
||||
"browser_scrollPositionsReaderMode.js",
|
||||
"browser_searchModeSwitcher_restore.js",
|
||||
"browser_speculative_connect.js",
|
||||
"browser_splitview_integer_ids.js",
|
||||
"browser_splitview_restore_in_closed_window.js",
|
||||
"browser_splitview_string_migration.js",
|
||||
"browser_swapDocShells.js",
|
||||
"browser_tab_groups_restore_closed_in_closed_window.js",
|
||||
"browser_tab_groups_restore_closed_in_open_window.js",
|
||||
"browser_tab_groups_restore_closed_many_tabs.js",
|
||||
"browser_tab_groups_closed.js",
|
||||
"browser_tab_groups_restore_multiple.js",
|
||||
"browser_tab_groups_restore_saved.js",
|
||||
"browser_tab_groups_restore_simple.js",
|
||||
"browser_tab_groups_save_on_removeAllTabsBut.js",
|
||||
"browser_tab_groups_save_on_removeTabsToTheEnd.js",
|
||||
"browser_tab_groups_save_on_removeTabsToTheStart.js",
|
||||
"browser_tab_groups_save_on_window_close.js",
|
||||
"browser_tab_groups_state.js",
|
||||
"browser_tab_groups_undo.js",
|
||||
"browser_tab_label_during_restore.js",
|
||||
"browser_undoCloseById.js",
|
||||
"browser_urlbarSearchMode.js",
|
||||
"browser_windowStateContainer.js",
|
||||
"browser_wireframe_basic.js",
|
||||
]
|
||||
|
||||
[sessionstore.replace-manifest]
|
||||
'prefs = [' = '''prefs = [
|
||||
"zen.window-sync.enabled=false",'''
|
||||
|
||||
[shell]
|
||||
source = "browser/components/shell/test"
|
||||
is_direct_path = true
|
||||
@@ -25,6 +133,15 @@ disable = [
|
||||
"browser_setDesktopBackgroundPreview.js",
|
||||
]
|
||||
|
||||
[sync]
|
||||
source = "services/sync/tests/unit"
|
||||
is_direct_path = true
|
||||
xpcshell = true
|
||||
|
||||
[sync.replace-manifest]
|
||||
"../../../" = "../../../../services/"
|
||||
'"identity.fxaccounts.enabled=true"' = '"identity.fxaccounts.enabled=true", "browser.formfill.enable=true"'
|
||||
|
||||
[tabMediaIndicator]
|
||||
source = "browser/components/tabbrowser/test/browser/tabMediaIndicator"
|
||||
is_direct_path = true
|
||||
@@ -34,3 +151,10 @@ source = "toolkit/components/tooltiptext"
|
||||
disable = [
|
||||
"browser_input_file_tooltips.js",
|
||||
]
|
||||
|
||||
[zoom]
|
||||
source = "browser/base/content/test/zoom"
|
||||
is_direct_path = true
|
||||
|
||||
[zoom.replace-manifest]
|
||||
"../general/" = "../../../../browser/base/content/test/general/"
|
||||
|
||||
32
src/zen/tests/mochitests/alerts/browser.toml
Normal file
32
src/zen/tests/mochitests/alerts/browser.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[DEFAULT]
|
||||
support-files = [
|
||||
"head.js",
|
||||
"file_dom_notifications.html",
|
||||
]
|
||||
|
||||
["browser_notification_close.js"]
|
||||
https_first_disabled = true
|
||||
skip-if = [
|
||||
"os == 'win'", # Bug 1227785
|
||||
]
|
||||
|
||||
["browser_notification_do_not_disturb.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
["browser_notification_open_settings.js"]
|
||||
https_first_disabled = true
|
||||
skip-if = [
|
||||
"os == 'win'", # Bug 1411118
|
||||
]
|
||||
|
||||
["browser_notification_remove_permission.js"]
|
||||
https_first_disabled = true
|
||||
skip-if = [
|
||||
"os == 'win'", # Bug 1411118
|
||||
]
|
||||
|
||||
["browser_notification_tab_switching.js"]
|
||||
https_first_disabled = true
|
||||
skip-if = [
|
||||
"os == 'win'", # Bug 1243263
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
|
||||
const { PlacesTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PlacesTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
let notificationURL =
|
||||
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||
|
||||
add_task(async function test_notificationClose() {
|
||||
await addNotificationPermission(notificationURL);
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: notificationURL,
|
||||
},
|
||||
async function dummyTabTask(aBrowser) {
|
||||
await openNotification(aBrowser, "showNotification2");
|
||||
|
||||
info("Notification alert showing");
|
||||
|
||||
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
if (!alertWindow) {
|
||||
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||
await closeNotification(aBrowser);
|
||||
return;
|
||||
}
|
||||
|
||||
let alertTitleLabel =
|
||||
alertWindow.document.getElementById("alertTitleLabel");
|
||||
is(
|
||||
alertTitleLabel.value,
|
||||
"Test title",
|
||||
"Title text of notification should be present"
|
||||
);
|
||||
let alertTextLabel =
|
||||
alertWindow.document.getElementById("alertTextLabel");
|
||||
is(
|
||||
alertTextLabel.textContent,
|
||||
"Test body 2",
|
||||
"Body text of notification should be present"
|
||||
);
|
||||
|
||||
let alertCloseButton = alertWindow.document.querySelector(".close-icon");
|
||||
is(alertCloseButton.localName, "toolbarbutton", "close button found");
|
||||
let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent(
|
||||
alertWindow,
|
||||
"beforeunload"
|
||||
);
|
||||
let closedTime = alertWindow.Date.now();
|
||||
alertCloseButton.click();
|
||||
info("Clicked on close button");
|
||||
await promiseBeforeUnloadEvent;
|
||||
|
||||
ok(true, "Alert should close when the close button is clicked");
|
||||
let currentTime = alertWindow.Date.now();
|
||||
// The notification will self-close at 12 seconds, so this checks
|
||||
// that the notification closed before the timeout.
|
||||
Assert.less(
|
||||
currentTime - closedTime,
|
||||
5000,
|
||||
"Close requested at " +
|
||||
closedTime +
|
||||
", actually closed at " +
|
||||
currentTime
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function cleanup() {
|
||||
PermissionTestUtils.remove(notificationURL, "desktop-notification");
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests that notifications can be silenced using nsIAlertsDoNotDisturb
|
||||
* on systems where that interface and its methods are implemented for
|
||||
* the nsIAlertService.
|
||||
*/
|
||||
|
||||
const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
|
||||
.getService(Ci.nsIAlertsService)
|
||||
.QueryInterface(Ci.nsIAlertsDoNotDisturb);
|
||||
|
||||
const PAGE =
|
||||
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||
|
||||
// The amount of time in seconds that we will wait for a notification
|
||||
// to show up before we decide that it's not coming.
|
||||
const NOTIFICATION_TIMEOUT_SECS = 2000;
|
||||
|
||||
add_setup(async function () {
|
||||
await addNotificationPermission(PAGE);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the manualDoNotDisturb attribute can prevent
|
||||
* notifications from appearing.
|
||||
*/
|
||||
add_task(async function test_manualDoNotDisturb() {
|
||||
try {
|
||||
// Only run the test if the do-not-disturb
|
||||
// interface has been implemented.
|
||||
ALERT_SERVICE.manualDoNotDisturb;
|
||||
ok(true, "Alert service implements do-not-disturb interface");
|
||||
} catch (e) {
|
||||
ok(
|
||||
true,
|
||||
"Alert service doesn't implement do-not-disturb interface, exiting test"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// In the event that something goes wrong during this test, make sure
|
||||
// we put the attribute back to the default setting when this test file
|
||||
// exits.
|
||||
registerCleanupFunction(() => {
|
||||
ALERT_SERVICE.manualDoNotDisturb = false;
|
||||
});
|
||||
|
||||
// Make sure that do-not-disturb is not enabled before we start.
|
||||
ok(
|
||||
!ALERT_SERVICE.manualDoNotDisturb,
|
||||
"Alert service should not be disabled when test starts"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.withNewTab(PAGE, async browser => {
|
||||
await openNotification(browser, "showNotification2");
|
||||
|
||||
info("Notification alert showing");
|
||||
|
||||
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
|
||||
// For now, only the XUL alert backend implements the manualDoNotDisturb
|
||||
// method for nsIAlertsDoNotDisturb, so we expect there to be a XUL alert
|
||||
// window. If the method gets implemented by native backends in the future,
|
||||
// we'll probably want to branch here and set the manualDoNotDisturb
|
||||
// attribute manually.
|
||||
ok(alertWindow, "Expected a XUL alert window.");
|
||||
|
||||
// We're using the XUL notification backend. This means that there's
|
||||
// a menuitem for enabling manualDoNotDisturb. We exercise that
|
||||
// menuitem here.
|
||||
let doNotDisturbMenuItem = alertWindow.document.getElementById(
|
||||
"doNotDisturbMenuItem"
|
||||
);
|
||||
is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found");
|
||||
|
||||
let unloadPromise = BrowserTestUtils.waitForEvent(
|
||||
alertWindow,
|
||||
"beforeunload"
|
||||
);
|
||||
|
||||
doNotDisturbMenuItem.click();
|
||||
info("Clicked on do-not-disturb menuitem");
|
||||
await unloadPromise;
|
||||
|
||||
// At this point, we should be configured to not display notifications
|
||||
// to the user.
|
||||
ok(
|
||||
ALERT_SERVICE.manualDoNotDisturb,
|
||||
"Alert service should be disabled after clicking menuitem"
|
||||
);
|
||||
|
||||
// The notification should not appear, but there is no way from the
|
||||
// client-side to know that it was blocked, except for waiting some time
|
||||
// and realizing that the "onshow" event never fired.
|
||||
await Assert.rejects(
|
||||
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
|
||||
/timed out/,
|
||||
"The notification should never display."
|
||||
);
|
||||
|
||||
ALERT_SERVICE.manualDoNotDisturb = false;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the suppressForScreenSharing attribute can prevent
|
||||
* notifications from appearing.
|
||||
*/
|
||||
add_task(async function test_suppressForScreenSharing() {
|
||||
try {
|
||||
// Only run the test if the do-not-disturb
|
||||
// interface has been implemented.
|
||||
ALERT_SERVICE.suppressForScreenSharing;
|
||||
ok(true, "Alert service implements do-not-disturb interface");
|
||||
} catch (e) {
|
||||
ok(
|
||||
true,
|
||||
"Alert service doesn't implement do-not-disturb interface, exiting test"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// In the event that something goes wrong during this test, make sure
|
||||
// we put the attribute back to the default setting when this test file
|
||||
// exits.
|
||||
registerCleanupFunction(() => {
|
||||
ALERT_SERVICE.suppressForScreenSharing = false;
|
||||
});
|
||||
|
||||
// Make sure that do-not-disturb is not enabled before we start.
|
||||
ok(
|
||||
!ALERT_SERVICE.suppressForScreenSharing,
|
||||
"Alert service should not be suppressing for screen sharing when test " +
|
||||
"starts"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.withNewTab(PAGE, async browser => {
|
||||
await openNotification(browser, "showNotification2");
|
||||
|
||||
info("Notification alert showing");
|
||||
await closeNotification(browser);
|
||||
ALERT_SERVICE.suppressForScreenSharing = true;
|
||||
|
||||
// The notification should not appear, but there is no way from the
|
||||
// client-side to know that it was blocked, except for waiting some time
|
||||
// and realizing that the "onshow" event never fired.
|
||||
await Assert.rejects(
|
||||
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
|
||||
/timed out/,
|
||||
"The notification should never display."
|
||||
);
|
||||
});
|
||||
|
||||
ALERT_SERVICE.suppressForScreenSharing = false;
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
|
||||
var notificationURL =
|
||||
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||
// BrowserGlue passes "privacy-permissions" to openPreferences when the
|
||||
// notifications-open-settings observer fires; the Settings Redesign
|
||||
// LegacyPaneMappings shim routes that to the permissionsData pane.
|
||||
var expectedURL = Services.prefs.getBoolPref(
|
||||
"browser.settings-redesign.enabled",
|
||||
false
|
||||
)
|
||||
? "about:preferences#permissionsData"
|
||||
: "about:preferences#privacy";
|
||||
|
||||
add_task(async function test_settingsOpen_observer() {
|
||||
info(
|
||||
"Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab."
|
||||
);
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: "about:robots",
|
||||
},
|
||||
async function dummyTabTask() {
|
||||
// Ensure preferences is loaded before removing the tab.
|
||||
let syncPaneLoadedPromise = TestUtils.topicObserved(
|
||||
"sync-pane-loaded",
|
||||
() => true
|
||||
);
|
||||
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
|
||||
info("simulate a notifications-open-settings notification");
|
||||
let uri = NetUtil.newURI("https://example.com");
|
||||
let principal = Services.scriptSecurityManager.createContentPrincipal(
|
||||
uri,
|
||||
{}
|
||||
);
|
||||
Services.obs.notifyObservers(principal, "notifications-open-settings");
|
||||
let tab = await tabPromise;
|
||||
ok(tab, "The notification settings tab opened");
|
||||
await syncPaneLoadedPromise;
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_settingsOpen_button() {
|
||||
info("Adding notification permission");
|
||||
await addNotificationPermission(notificationURL);
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: notificationURL,
|
||||
},
|
||||
async function tabTask(aBrowser) {
|
||||
info("Waiting for notification");
|
||||
await openNotification(aBrowser, "showNotification2");
|
||||
|
||||
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
if (!alertWindow) {
|
||||
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||
await closeNotification(aBrowser);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure preferences is loaded before removing the tab.
|
||||
let syncPaneLoadedPromise = TestUtils.topicObserved(
|
||||
"sync-pane-loaded",
|
||||
() => true
|
||||
);
|
||||
let closePromise = promiseWindowClosed(alertWindow);
|
||||
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
|
||||
let openSettingsMenuItem = alertWindow.document.getElementById(
|
||||
"openSettingsMenuItem"
|
||||
);
|
||||
openSettingsMenuItem.click();
|
||||
|
||||
info("Waiting for notification settings tab");
|
||||
let tab = await tabPromise;
|
||||
ok(tab, "The notification settings tab opened");
|
||||
|
||||
await syncPaneLoadedPromise;
|
||||
await closePromise;
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
|
||||
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
var tab;
|
||||
var notificationURL =
|
||||
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||
var alertWindowClosed = false;
|
||||
var permRemoved = false;
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
gBrowser.removeTab(tab);
|
||||
window.restore();
|
||||
});
|
||||
|
||||
addNotificationPermission(notificationURL).then(function openTab() {
|
||||
tab = BrowserTestUtils.addTab(gBrowser, notificationURL);
|
||||
gBrowser.selectedTab = tab;
|
||||
BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => onLoad());
|
||||
});
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing);
|
||||
}
|
||||
|
||||
function onAlertShowing() {
|
||||
info("Notification alert showing");
|
||||
|
||||
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
if (!alertWindow) {
|
||||
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||
closeNotification(tab.linkedBrowser).then(finish);
|
||||
return;
|
||||
}
|
||||
ok(
|
||||
PermissionTestUtils.testExactPermission(
|
||||
notificationURL,
|
||||
"desktop-notification"
|
||||
),
|
||||
"Permission should exist prior to removal"
|
||||
);
|
||||
let disableForOriginMenuItem = alertWindow.document.getElementById(
|
||||
"disableForOriginMenuItem"
|
||||
);
|
||||
is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
|
||||
Services.obs.addObserver(permObserver, "perm-changed");
|
||||
alertWindow.addEventListener("beforeunload", onAlertClosing);
|
||||
disableForOriginMenuItem.click();
|
||||
info("Clicked on disable-for-origin menuitem");
|
||||
}
|
||||
|
||||
function permObserver(subject, topic, data) {
|
||||
if (topic != "perm-changed") {
|
||||
return;
|
||||
}
|
||||
|
||||
let permission = subject.QueryInterface(Ci.nsIPermission);
|
||||
is(
|
||||
permission.type,
|
||||
"desktop-notification",
|
||||
"desktop-notification permission changed"
|
||||
);
|
||||
is(data, "deleted", "desktop-notification permission deleted");
|
||||
|
||||
Services.obs.removeObserver(permObserver, "perm-changed");
|
||||
permRemoved = true;
|
||||
if (alertWindowClosed) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
function onAlertClosing(event) {
|
||||
event.target.removeEventListener("beforeunload", onAlertClosing);
|
||||
|
||||
alertWindowClosed = true;
|
||||
if (permRemoved) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
var tab;
|
||||
var notification;
|
||||
var notificationURL =
|
||||
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||
var newWindowOpenedFromTab;
|
||||
|
||||
add_task(async function test_notificationPreventDefaultAndSwitchTabs() {
|
||||
await addNotificationPermission(notificationURL);
|
||||
|
||||
let originalTab = gBrowser.selectedTab;
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: notificationURL,
|
||||
},
|
||||
async function dummyTabTask(aBrowser) {
|
||||
// Put new tab in background so it is obvious when it is re-focused.
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
isnot(
|
||||
gBrowser.selectedBrowser,
|
||||
aBrowser,
|
||||
"Notification page loaded as a background tab"
|
||||
);
|
||||
|
||||
// First, show a notification that will be have the tab-switching prevented.
|
||||
function promiseNotificationEvent(evt) {
|
||||
return SpecialPowers.spawn(
|
||||
aBrowser,
|
||||
[evt],
|
||||
async function (contentEvt) {
|
||||
return new Promise(resolve => {
|
||||
let contentNotification = content.wrappedJSObject._notification;
|
||||
contentNotification.addEventListener(
|
||||
contentEvt,
|
||||
function (event) {
|
||||
resolve({ defaultPrevented: event.defaultPrevented });
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
await openNotification(aBrowser, "showNotification1");
|
||||
info("Notification alert showing");
|
||||
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
if (!alertWindow) {
|
||||
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||
await closeNotification(aBrowser);
|
||||
return;
|
||||
}
|
||||
info("Clicking on notification");
|
||||
let promiseClickEvent = promiseNotificationEvent("click");
|
||||
|
||||
// NB: This executeSoon is needed to allow the non-e10s runs of this test
|
||||
// a chance to set the event listener on the page. Otherwise, we
|
||||
// synchronously fire the click event before we listen for the event.
|
||||
executeSoon(() => {
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
alertWindow.document.getElementById("alertTitleLabel"),
|
||||
{},
|
||||
alertWindow
|
||||
);
|
||||
});
|
||||
let clickEvent = await promiseClickEvent;
|
||||
ok(
|
||||
clickEvent.defaultPrevented,
|
||||
"The event handler for the first notification cancels the event"
|
||||
);
|
||||
isnot(
|
||||
gBrowser.selectedBrowser,
|
||||
aBrowser,
|
||||
"Notification page still a background tab"
|
||||
);
|
||||
let notificationClosed = promiseNotificationEvent("close");
|
||||
await closeNotification(aBrowser);
|
||||
await notificationClosed;
|
||||
|
||||
// Second, show a notification that will cause the tab to get switched.
|
||||
await openNotification(aBrowser, "showNotification2");
|
||||
alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||
let promiseTabSelect = BrowserTestUtils.waitForEvent(
|
||||
gBrowser.tabContainer,
|
||||
"TabSelect"
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
alertWindow.document.getElementById("alertTitleLabel"),
|
||||
{},
|
||||
alertWindow
|
||||
);
|
||||
await promiseTabSelect;
|
||||
is(
|
||||
gBrowser.selectedBrowser.currentURI.spec,
|
||||
notificationURL,
|
||||
"Clicking on the second notification should select its originating tab"
|
||||
);
|
||||
notificationClosed = promiseNotificationEvent("close");
|
||||
await closeNotification(aBrowser);
|
||||
await notificationClosed;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function cleanup() {
|
||||
PermissionTestUtils.remove(notificationURL, "desktop-notification");
|
||||
});
|
||||
39
src/zen/tests/mochitests/alerts/file_dom_notifications.html
Normal file
39
src/zen/tests/mochitests/alerts/file_dom_notifications.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
function showNotification1() {
|
||||
var options = {
|
||||
dir: undefined,
|
||||
lang: undefined,
|
||||
body: "Test body 1",
|
||||
tag: "Test tag",
|
||||
icon: undefined,
|
||||
};
|
||||
var n = new Notification("Test title", options);
|
||||
n.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
function showNotification2() {
|
||||
var options = {
|
||||
dir: undefined,
|
||||
lang: undefined,
|
||||
body: "Test body 2",
|
||||
tag: "Test tag",
|
||||
icon: undefined,
|
||||
};
|
||||
return new Notification("Test title", options);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<form id="notificationForm" onsubmit="showNotification();">
|
||||
<input type="submit" value="Show notification" id="submit"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
73
src/zen/tests/mochitests/alerts/head.js
Normal file
73
src/zen/tests/mochitests/alerts/head.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// Platforms may default to reducing motion. We override this to ensure the
|
||||
// alert slide animation is enabled in tests.
|
||||
SpecialPowers.pushPrefEnv({
|
||||
set: [["ui.prefersReducedMotion", 0]],
|
||||
});
|
||||
|
||||
async function addNotificationPermission(originString) {
|
||||
return SpecialPowers.pushPermissions([
|
||||
{
|
||||
type: "desktop-notification",
|
||||
allow: true,
|
||||
context: originString,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `BrowserTestUtils.closeWindow`, but
|
||||
* doesn't call `window.close()`.
|
||||
*/
|
||||
function promiseWindowClosed(window) {
|
||||
return new Promise(function (resolve) {
|
||||
Services.ww.registerNotification(function observer(subject, topic) {
|
||||
if (topic == "domwindowclosed" && subject == window) {
|
||||
Services.ww.unregisterNotification(observer);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* These two functions work with file_dom_notifications.html to open the
|
||||
* notification and close it.
|
||||
*
|
||||
* |fn| can be showNotification1 or showNotification2.
|
||||
* if |timeout| is passed, then the promise returned from this function is
|
||||
* rejected after the requested number of miliseconds.
|
||||
*/
|
||||
function openNotification(aBrowser, fn, timeout) {
|
||||
info(`openNotification: ${fn}`);
|
||||
return SpecialPowers.spawn(
|
||||
aBrowser,
|
||||
[fn, timeout],
|
||||
async function (contentFn, contentTimeout) {
|
||||
await new Promise((resolve, reject) => {
|
||||
let win = content.wrappedJSObject;
|
||||
let notification = win[contentFn]();
|
||||
win._notification = notification;
|
||||
|
||||
function listener() {
|
||||
notification.removeEventListener("show", listener);
|
||||
resolve();
|
||||
}
|
||||
|
||||
notification.addEventListener("show", listener);
|
||||
|
||||
if (contentTimeout) {
|
||||
content.setTimeout(() => {
|
||||
notification.removeEventListener("show", listener);
|
||||
reject("timed out");
|
||||
}, contentTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function closeNotification(aBrowser) {
|
||||
return SpecialPowers.spawn(aBrowser, [], function () {
|
||||
content.wrappedJSObject._notification.close();
|
||||
});
|
||||
}
|
||||
7
src/zen/tests/mochitests/backforward/browser.toml
Normal file
7
src/zen/tests/mochitests/backforward/browser.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
["browser_history_menu.js"]
|
||||
fail-if = [
|
||||
"a11y_checks", # Bug 1854233 navigator-toolbox may not be focusable
|
||||
]
|
||||
https_first_disabled = true
|
||||
179
src/zen/tests/mochitests/backforward/browser_history_menu.js
Normal file
179
src/zen/tests/mochitests/backforward/browser_history_menu.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// This test verifies that the back forward button long-press menu and context menu
|
||||
// shows the correct history items.
|
||||
|
||||
add_task(async function mousedown_back() {
|
||||
await testBackForwardMenu(false);
|
||||
});
|
||||
|
||||
add_task(async function contextmenu_back() {
|
||||
await testBackForwardMenu(true);
|
||||
});
|
||||
|
||||
async function openHistoryMenu(useContextMenu) {
|
||||
let backButton = document.getElementById("back-button");
|
||||
let rect = backButton.getBoundingClientRect();
|
||||
|
||||
info("waiting for the history menu to open");
|
||||
|
||||
let popupShownPromise = BrowserTestUtils.waitForEvent(
|
||||
useContextMenu ? document.getElementById("backForwardMenu") : backButton,
|
||||
"popupshown"
|
||||
);
|
||||
if (useContextMenu) {
|
||||
EventUtils.synthesizeMouseAtCenter(backButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
} else {
|
||||
EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" });
|
||||
}
|
||||
|
||||
EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {
|
||||
type: "mouseup",
|
||||
});
|
||||
let popupEvent = await popupShownPromise;
|
||||
|
||||
ok(true, "history menu opened");
|
||||
|
||||
return popupEvent;
|
||||
}
|
||||
|
||||
async function testBackForwardMenu(useContextMenu) {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||
"http://example.com"
|
||||
);
|
||||
|
||||
for (let iter = 2; iter <= 4; iter++) {
|
||||
// Iterate three times. For the first two times through the loop, add a new history item.
|
||||
// But for the last iteration, go back in the history instead.
|
||||
await SpecialPowers.spawn(
|
||||
gBrowser.selectedBrowser,
|
||||
[iter],
|
||||
async function (iterChild) {
|
||||
if (iterChild == 4) {
|
||||
let popStatePromise = new Promise(function (resolve) {
|
||||
content.onpopstate = resolve;
|
||||
});
|
||||
content.history.back();
|
||||
await popStatePromise;
|
||||
} else {
|
||||
// With the back-button intervention enabled, we need to make sure to
|
||||
// trigger a user activation on each history entry, or they won't
|
||||
// show in the menu.
|
||||
content.document.notifyUserGestureActivation();
|
||||
content.history.pushState({}, "" + iterChild, iterChild + ".html");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Wait for the session data to be flushed before continuing the test
|
||||
await new Promise(resolve =>
|
||||
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||
);
|
||||
|
||||
let popupEvent = await openHistoryMenu(useContextMenu);
|
||||
|
||||
// Wait for the session data to be flushed before continuing the test
|
||||
await new Promise(resolve =>
|
||||
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||
);
|
||||
|
||||
is(
|
||||
popupEvent.target.children.length,
|
||||
iter > 3 ? 3 : iter,
|
||||
"Correct number of history items"
|
||||
);
|
||||
|
||||
let node = popupEvent.target.lastElementChild;
|
||||
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||
is(node.getAttribute("uri"), "http://example.com/", "'1' item uri");
|
||||
is(node.getAttribute("index"), "0", "'1' item index");
|
||||
is(
|
||||
node.getAttribute("historyindex"),
|
||||
iter == 3 ? "-2" : "-1",
|
||||
"'1' item historyindex"
|
||||
);
|
||||
|
||||
node = node.previousElementSibling;
|
||||
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||
is(node.getAttribute("uri"), "http://example.com/2.html", "'2' item uri");
|
||||
is(node.getAttribute("index"), "1", "'2' item index");
|
||||
is(
|
||||
node.getAttribute("historyindex"),
|
||||
iter == 3 ? "-1" : "0",
|
||||
"'2' item historyindex"
|
||||
);
|
||||
|
||||
if (iter >= 3) {
|
||||
node = node.previousElementSibling;
|
||||
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||
is(node.getAttribute("uri"), "http://example.com/3.html", "'3' item uri");
|
||||
is(node.getAttribute("index"), "2", "'3' item index");
|
||||
is(
|
||||
node.getAttribute("historyindex"),
|
||||
iter == 4 ? "1" : "0",
|
||||
"'3' item historyindex"
|
||||
);
|
||||
}
|
||||
|
||||
// Close the popup, but on the last iteration, click on one of the history items
|
||||
// to ensure it opens in a new tab.
|
||||
let popupHiddenPromise = BrowserTestUtils.waitForEvent(
|
||||
popupEvent.target,
|
||||
"popuphidden"
|
||||
);
|
||||
|
||||
if (iter < 4) {
|
||||
popupEvent.target.hidePopup();
|
||||
} else {
|
||||
let newTabPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||
url => url == "http://example.com/"
|
||||
);
|
||||
|
||||
popupEvent.target.activateItem(popupEvent.target.children[2], {
|
||||
button: 1,
|
||||
});
|
||||
|
||||
let newtab = await newTabPromise;
|
||||
gBrowser.removeTab(newtab);
|
||||
}
|
||||
|
||||
await popupHiddenPromise;
|
||||
}
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
|
||||
// Make sure that the history popup appears after navigating around in a preferences page.
|
||||
add_task(async function test_preferences_page() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:preferences"
|
||||
);
|
||||
|
||||
openPreferences("search");
|
||||
let popupEvent = await openHistoryMenu(true);
|
||||
|
||||
// Wait for the session data to be flushed before continuing the test
|
||||
await new Promise(resolve =>
|
||||
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||
);
|
||||
|
||||
is(popupEvent.target.children.length, 2, "Correct number of history items");
|
||||
|
||||
let popupHiddenPromise = BrowserTestUtils.waitForEvent(
|
||||
popupEvent.target,
|
||||
"popuphidden"
|
||||
);
|
||||
popupEvent.target.hidePopup();
|
||||
await popupHiddenPromise;
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user