Compare commits

...

14 Commits

Author SHA1 Message Date
trop[bot]
93387921c0 fix: webContents.print() ignoring mediaSize when silent (#50856)
fix: webContents.print() ignoring mediaSize when silent

PR #49523 moved the default media size fallback into OnGetDeviceNameToUse,
but the new code unconditionally writes kSettingMediaSize — clobbering
any mediaSize the caller had already set in WebContents::Print() from
options.mediaSize / pageSize. As a result, silent prints with an
explicit pageSize (e.g. "Letter") fell back to A4 with tiny content.

Only populate the default/printer media size when the caller hasn't
already supplied one, preserving the precedence:
  1. user-supplied mediaSize / pageSize
  2. printer default (when usePrinterDefaultPageSize is true)
  3. A4 fallback

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-09 21:02:25 -05:00
trop[bot]
0bb55bb6f2 fix: move Electron help menu links to default app only (#50859)
fix: move Electron help menu links to default app only (#50629)

* fix: remove Electron links from default help menu

* fix: remove help menu entirely from default menu

* fix: move Electron help menu links to default app

* docs: update default menu items list in menu.md

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Zeenat Lawal <zeenatlawal82@gmail.com>
2026-04-09 15:13:32 -07:00
electron-roller[bot]
e9e28f4f8f chore: bump node to v24.14.1 (41-x-y) (#50478)
* chore: bump node in DEPS to v24.14.1

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-04-09 12:23:32 -04:00
electron-roller[bot]
f2cbab1115 chore: bump chromium to 146.0.7680.188 (41-x-y) (#50787)
* chore: bump chromium in DEPS to 146.0.7680.180

* chore: bump chromium in DEPS to 146.0.7680.188

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-04-09 15:32:20 +02:00
trop[bot]
9042667690 fix: account for extraSize in aspect ratio min/max clamping on macOS (#50835)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: clavin <clavin@electronjs.org>
2026-04-09 15:31:09 +02:00
trop[bot]
1ad20e5ba4 fix: menu items not cleaned up after rebuild (#50830)
Menu was holding a SelfKeepAlive to itself from construction, so any
Menu that was never opened (e.g. an application menu replaced before
being shown) stayed pinned in cppgc forever. Repeated calls to
Menu.setApplicationMenu leaked every prior Menu along with its model
and items.

Restore the original Pin/Unpin lifecycle: start keep_alive_ empty and
only assign `this` in OnMenuWillShow. OnMenuWillClose already clears
it.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-09 11:56:38 +02:00
trop[bot]
a26aee5a6d fix: devtools re-attaches on open when previously detached (#50816)
PR #50646 added a dock state allowlist in SetDockState() that collapsed any
non-matching value to "right". WebContents::OpenDevTools passes an empty
string when no `mode` option is given, which is the sentinel LoadCompleted()
uses to restore `currentDockState` from prefs. The allowlist clobbered that
sentinel to "right", so previously-undocked devtools would flash detached
and then snap back to the right dock.

Preserve the empty string through SetDockState() so the pref-restore path
runs; still reject any non-empty invalid value to keep the JS-injection
guard from #50646 intact.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-08 13:06:40 -07:00
trop[bot]
7394591138 ci: use hermetic mac SDK for the release ffmpeg build (#50755)
* ci: use hermetic mac SDK for the release ffmpeg build

gn gen out/ffmpeg runs as a raw gn invocation, so it never receives the
mac_sdk_path arg that e build injects for out/Default. On macOS runners
that means out/Default builds against the hermetic build-tools SDK while
out/ffmpeg falls through to the runner's system Xcode SDK. Reuse the
value e build already wrote so both builds share the same sysroot.

Co-authored-by: Samuel Attard <sattard@anthropic.com>

* ci: copy hermetic SDK symlink into out/ffmpeg and rewrite path

mac_sdk_path must live under root_build_dir, so pointing out/ffmpeg at
//out/Default/... doesn't work. Copy the xcode_links symlink tree into
out/ffmpeg and rewrite the path. Gate on Darwin so Windows/Linux don't
run the sed/cp at all.

Co-authored-by: Samuel Attard <sattard@anthropic.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <sattard@anthropic.com>
2026-04-06 21:35:50 -04:00
trop[bot]
d37b4f5d9f fix: enforce size constraints on window creation on Windows and Linux (#50753)
fix: enforce size constraints on window creation on Windows and Linux (#49906)

* enforce size constraints on window creation

* set constraints after resizing on init

* restore conditional centering

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Mitchell Cohen <mitch.cohen@me.com>
2026-04-06 18:40:05 -05:00
trop[bot]
6f1d53ae8f ci: make src-cache upload atomic (#50750)
ci: make src-cache upload atomic and sweep orphaned temp files

The checkout action's cp of the ~6GB zstd archive directly to the final
path on the cache share is non-atomic; an interrupted copy or a
concurrent reader produces zstd "Read error (39): premature end" on
restore, and the truncated file then satisfies the existence check so
no later run repairs it.

Upload to a run-unique *.tar.upload-<run_id>-<attempt> temp name on the
share and mv to the final path, discarding our temp if a concurrent run
got there first. A new clean-orphaned-cache-uploads workflow removes
temp files older than 4h every 4 hours.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-04-06 22:39:35 +00:00
trop[bot]
fb150b2f17 docs: link menu type references (#50752)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: lilianakatrina684-a11y <lilianakatrina684@gmail.com>
2026-04-06 17:27:35 -04:00
Samuel Attard
c219f2c990 build: derive patches upstream-head ref from script path (41-x-y) (#50741)
build: derive patches upstream-head ref from script path (#50727)

* build: derive patches upstream-head ref from script path

gclient-new-workdir.py symlinks each repo's .git/refs back to the source
checkout, so the fixed refs/patches/upstream-head was shared across all
worktrees. Parallel `e sync` runs in different worktrees clobbered each
other's upstream-head, breaking `e patches` and check-patch-diff.

Suffix the ref with an md5 of the script directory so each worktree writes
a distinct ref into the shared refs dir. Fall back to the legacy ref name
in guess_base_commit so existing checkouts keep working until next sync.

* fixup: also write legacy upstream-head ref and note it in docs
2026-04-06 16:02:11 -04:00
Samuel Attard
3fa5280fde fix: re-enable MacWebContentsOcclusion with embedder window fix (#50715)
fix: re-enable MacWebContentsOcclusion with embedder window fix (#50579)

* fix: re-enable MacWebContentsOcclusion with embedder window fix

Replace the full revert of Chromium's MacWebContentsOcclusion cleanup
with a targeted patch that handles embedder windows shown after
WebContentsViewCocoa attachment. This lets us drop the feature flag
disable in feature_list.cc and re-enable upstream occlusion tracking.

Adds tests for show/hide event counts on macOS and visibility tracking
across multiple child WebContentsViews.

* test: drop show/hide event count assertion

The assertion that 'show' fires exactly once per w.show() call is not
an API guarantee - macOS can send multiple occlusion state
notifications during a single show() when other windows are on screen
(common on CI after hundreds of prior tests). The
visibilitychange-count test in api-web-contents-view-spec.ts covers
the actual invariant we care about.

* fix: ignore WebContentsOcclusionCheckerMac synthetic notifications in window delegate

On macOS 13.3-25.x, Chromium's occlusion checker enables manual
frame-intersection detection and posts synthetic
NSWindowDidChangeOcclusionStateNotification tagged with its class name
in userInfo. These fire when the checker's NSContainsRect heuristic
decides a window is covered by another window's frame, but the real
-[NSWindow occlusionState] hasn't changed.

Our delegate was treating these the same as real macOS notifications
and emitting show/hide events based on occlusionState, which was
unchanged - resulting in spurious duplicate show events when e.g.
Quick Look opened and its frame intersected the BrowserWindow.
2026-04-06 16:01:41 -04:00
electron-roller[bot]
45ad6b3525 chore: bump chromium to 146.0.7680.179 (41-x-y) (#50616)
* chore: bump chromium in DEPS to 146.0.7680.178

* chore: bump chromium in DEPS to 146.0.7680.179

* chore: fixup patch indices

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-06 15:03:38 -04:00
31 changed files with 418 additions and 413 deletions

View File

@@ -205,7 +205,17 @@ runs:
if: ${{ inputs.is-release == 'true' }}
run: |
cd src
gn gen out/ffmpeg --args="import(\"//electron/build/args/ffmpeg.gn\") use_remoteexec=true use_siso=true $GN_EXTRA_ARGS"
# Reuse the hermetic mac_sdk_path that `e build` wrote for out/Default so
# out/ffmpeg builds against the same SDK instead of the runner's system Xcode.
# The path has to live under root_build_dir, so copy the symlink tree and
# rewrite Default -> ffmpeg.
MAC_SDK_ARG=""
if [ "$(uname)" = "Darwin" ]; then
mkdir -p out/ffmpeg
cp -a out/Default/xcode_links out/ffmpeg/
MAC_SDK_ARG=$(sed -n 's|^\(mac_sdk_path = "//out/\)Default/|\1ffmpeg/|p' out/Default/args.gn)
fi
gn gen out/ffmpeg --args="import(\"//electron/build/args/ffmpeg.gn\") use_remoteexec=true use_siso=true $MAC_SDK_ARG $GN_EXTRA_ARGS"
e build --target electron:electron_ffmpeg_zip -C ../../out/ffmpeg
- name: Remove Clang problem matcher
shell: bash

View File

@@ -191,19 +191,31 @@ runs:
# only permits .tar/.tgz so we keep the extension and decode on restore.
tar -cf - src | zstd -T0 --long=30 -f -o $CACHE_FILE
echo "Compressed src to $(du -sh $CACHE_FILE | cut -f1 -d' ')"
cp ./$CACHE_FILE $CACHE_DRIVE/
- name: Persist Src Cache
if: ${{ steps.check-cache.outputs.cache_exists == 'false' && inputs.use-cache == 'true' }}
shell: bash
run: |
final_cache_path=$CACHE_DRIVE/$CACHE_FILE
# Upload to a run-unique temp name first so concurrent readers never
# observe a partially-written file, and an interrupted copy can't leave
# a truncated file at the final path. Orphaned temp files get swept by
# the clean-orphaned-cache-uploads workflow.
tmp_cache_path=$final_cache_path.upload-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}
echo "Uploading to temp path: $tmp_cache_path"
cp ./$CACHE_FILE $tmp_cache_path
echo "Using cache key: $DEPSHASH"
echo "Checking path: $final_cache_path"
if [ -f "$final_cache_path" ]; then
echo "Cache already persisted at $final_cache_path by a concurrent run; discarding ours"
rm -f $tmp_cache_path
else
mv -f $tmp_cache_path $final_cache_path
echo "Cache key persisted in $final_cache_path"
fi
if [ ! -f "$final_cache_path" ]; then
echo "Cache key not found"
exit 1
else
echo "Cache key persisted in $final_cache_path"
fi
- name: Wait for active SSH sessions
shell: bash

View File

@@ -0,0 +1,32 @@
name: Clean Orphaned Cache Uploads
# Description:
# Sweeps orphaned in-flight upload temp files left on the src-cache volumes
# by checkout/action.yml when its cp-to-share step dies before the rename.
# A successful upload finishes in minutes, so anything older than 4h is dead.
on:
schedule:
- cron: "0 */4 * * *"
workflow_dispatch:
permissions: {}
jobs:
clean-orphaned-uploads:
if: github.repository == 'electron/electron'
runs-on: electron-arc-centralus-linux-amd64-32core
permissions:
contents: read
container:
image: ghcr.io/electron/build:bc2f48b2415a670de18d13605b1cf0eb5fdbaae1
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
- /mnt/win-cache:/mnt/win-cache
steps:
- name: Remove Orphaned Upload Temp Files
shell: bash
run: |
find /mnt/cross-instance-cache -maxdepth 1 -type f -name '*.tar.upload-*' -mmin +240 -print -delete
find /mnt/win-cache -maxdepth 1 -type f -name '*.tar.upload-*' -mmin +240 -print -delete

4
DEPS
View File

@@ -2,9 +2,9 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'146.0.7680.166',
'146.0.7680.188',
'node_version':
'v24.14.0',
'v24.14.1',
'nan_version':
'675cefebca42410733da8a454c8d9391fcebfbc2',
'squirrel.mac_version':

View File

@@ -1,5 +1,5 @@
import { shell } from 'electron/common';
import { app, dialog, BrowserWindow, ipcMain } from 'electron/main';
import { app, dialog, BrowserWindow, ipcMain, Menu } from 'electron/main';
import * as path from 'node:path';
import * as url from 'node:url';
@@ -11,6 +11,53 @@ app.on('window-all-closed', () => {
app.quit();
});
const isMac = process.platform === 'darwin';
app.whenReady().then(() => {
const helpMenu: Electron.MenuItemConstructorOptions = {
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
}
},
{
label: 'Documentation',
click: async () => {
const version = process.versions.electron;
await shell.openExternal(`https://github.com/electron/electron/tree/v${version}/docs#readme`);
}
},
{
label: 'Community Discussions',
click: async () => {
await shell.openExternal('https://discord.gg/electronjs');
}
},
{
label: 'Search Issues',
click: async () => {
await shell.openExternal('https://github.com/electron/electron/issues');
}
}
]
};
const macAppMenu: Electron.MenuItemConstructorOptions = { role: 'appMenu' };
const template: Electron.MenuItemConstructorOptions[] = [
...(isMac ? [macAppMenu] : []),
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' },
helpMenu
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});
function decorateURL (url: string) {
// safely add `?utm_source=default_app
const parsedUrl = new URL(url);

View File

@@ -44,8 +44,8 @@ See [`Menu`](menu.md) for examples.
menu items.
* `registerAccelerator` boolean (optional) _Linux_ _Windows_ - If false, the accelerator won't be registered
with the system, but it will still be displayed. Defaults to true.
* `sharingItem` SharingItem (optional) _macOS_ - The item to share when the `role` is `shareMenu`.
* `submenu` (MenuItemConstructorOptions[] | [Menu](menu.md)) (optional) - Should be specified
* `sharingItem` [SharingItem](structures/sharing-item.md) (optional) _macOS_ - The item to share when the `role` is `shareMenu`.
* `submenu` ([MenuItemConstructorOptions](#new-menuitemoptions)[] | [Menu](menu.md)) (optional) - Should be specified
for `submenu` type menu items. If `submenu` is specified, the `type: 'submenu'` can be omitted.
If the value is not a [`Menu`](menu.md) then it will be automatically converted to one using
`Menu.buildFromTemplate`.
@@ -89,7 +89,7 @@ A `Function` that is fired when the MenuItem receives a click event.
It can be called with `menuItem.click(event, focusedWindow, focusedWebContents)`.
* `event` [KeyboardEvent](structures/keyboard-event.md)
* `focusedWindow` [BaseWindow](browser-window.md)
* `focusedWindow` [BaseWindow](base-window.md)
* `focusedWebContents` [WebContents](web-contents.md)
#### `menuItem.submenu`
@@ -110,11 +110,11 @@ A `string` (optional) indicating the item's role, if set. Can be `undo`, `redo`,
#### `menuItem.accelerator`
An `Accelerator | null` indicating the item's accelerator, if set.
An [`Accelerator | null`](../tutorial/keyboard-shortcuts.md#accelerators) indicating the item's accelerator, if set.
#### `menuItem.userAccelerator` _Readonly_ _macOS_
An `Accelerator | null` indicating the item's [user-assigned accelerator](https://developer.apple.com/documentation/appkit/nsmenuitem/1514850-userkeyequivalent?language=objc) for the menu item.
An [`Accelerator | null`](../tutorial/keyboard-shortcuts.md#accelerators) indicating the item's [user-assigned accelerator](https://developer.apple.com/documentation/appkit/nsmenuitem/1514850-userkeyequivalent?language=objc) for the menu item.
> [!NOTE]
> This property is only initialized after the `MenuItem` has been added to a `Menu`. Either via `Menu.buildFromTemplate` or via `Menu.append()/insert()`. Accessing before initialization will just return `null`.
@@ -170,7 +170,7 @@ This property can be dynamically changed.
#### `menuItem.sharingItem` _macOS_
A `SharingItem` indicating the item to share when the `role` is `shareMenu`.
A [`SharingItem`](structures/sharing-item.md) indicating the item to share when the `role` is `shareMenu`.
This property can be dynamically changed.

View File

@@ -46,7 +46,7 @@ this has the additional effect of removing the menu bar from the window.
> [!NOTE]
> The default menu will be created automatically if the app does not set one.
> It contains standard items such as `File`, `Edit`, `View`, `Window` and `Help`.
> It contains standard items such as `File`, `Edit`, `View`, and `Window`.
#### `Menu.getApplicationMenu()`
@@ -70,7 +70,7 @@ for more information on macOS' native actions.
#### `Menu.buildFromTemplate(template)`
- `template` (MenuItemConstructorOptions | [MenuItem](menu-item.md))[]
- `template` ([MenuItemConstructorOptions](menu-item.md#new-menuitemoptions) | [MenuItem](menu-item.md))[]
Returns [`Menu`](menu.md)
@@ -162,7 +162,7 @@ Emitted when a popup is closed either manually or with `menu.closePopup()`.
#### `menu.items`
A `MenuItem[]` array containing the menu's items.
A [`MenuItem[]`](menu-item.md) array containing the menu's items.
Each `Menu` consists of multiple [`MenuItem`](menu-item.md) instances and each `MenuItem`
can nest a `Menu` into its `submenu` property.

View File

@@ -79,7 +79,7 @@ $ ../../electron/script/git-import-patches ../../electron/patches/node
$ ../../electron/script/git-export-patches -o ../../electron/patches/node
```
Note that `git-import-patches` will mark the commit that was `HEAD` when it was run as `refs/patches/upstream-head`. This lets you keep track of which commits are from Electron patches (those that come after `refs/patches/upstream-head`) and which commits are in upstream (those before `refs/patches/upstream-head`).
Note that `git-import-patches` will mark the commit that was `HEAD` when it was run as `refs/patches/upstream-head` (and a checkout-specific `refs/patches/upstream-head-<hash>` so that gclient worktrees sharing a `.git/refs` directory don't clobber each other). This lets you keep track of which commits are from Electron patches (those that come after `refs/patches/upstream-head`) and which commits are in upstream (those before `refs/patches/upstream-head`).
#### Resolving conflicts

View File

@@ -1,5 +1,4 @@
import { shell } from 'electron/common';
import { app, Menu } from 'electron/main';
import { Menu } from 'electron/main';
const isMac = process.platform === 'darwin';
@@ -12,47 +11,13 @@ export const setApplicationMenuWasSet = () => {
export const setDefaultApplicationMenu = () => {
if (applicationMenuWasSet) return;
const helpMenu: Electron.MenuItemConstructorOptions = {
role: 'help',
submenu: app.isPackaged
? []
: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
}
},
{
label: 'Documentation',
click: async () => {
const version = process.versions.electron;
await shell.openExternal(`https://github.com/electron/electron/tree/v${version}/docs#readme`);
}
},
{
label: 'Community Discussions',
click: async () => {
await shell.openExternal('https://discord.gg/electronjs');
}
},
{
label: 'Search Issues',
click: async () => {
await shell.openExternal('https://github.com/electron/electron/issues');
}
}
]
};
const macAppMenu: Electron.MenuItemConstructorOptions = { role: 'appMenu' };
const template: Electron.MenuItemConstructorOptions[] = [
...(isMac ? [macAppMenu] : []),
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' },
helpMenu
{ role: 'windowMenu' }
];
const menu = Menu.buildFromTemplate(template);

View File

@@ -121,7 +121,7 @@ build_disable_thin_lto_mac.patch
feat_corner_smoothing_css_rule_and_blink_painting.patch
build_add_public_config_simdutf_config.patch
fix_multiple_scopedpumpmessagesinprivatemodes_instances.patch
revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
fix_handle_embedder_windows_shown_after_webcontentsviewcocoa_attach.patch
feat_add_signals_when_embedder_cleanup_callbacks_run_for.patch
feat_separate_content_settings_callback_for_sync_and_async_clipboard.patch
fix_win32_synchronous_spellcheck.patch

View File

@@ -43,7 +43,7 @@ index 21d5ab99800c0830cc31ec4ebb24e3f05cd904d8..3f8f514519d6e4a0abe3690f5df35de8
// When the enterprise policy is not set, use finch/feature flag choice.
return base::FeatureList::IsEnabled(chrome_pdf::features::kPdfXfaSupport);
diff --git a/chrome/browser/pdf/pdf_extension_util.cc b/chrome/browser/pdf/pdf_extension_util.cc
index 83bc44f0c1928b9023efa54bfb57bed69d77484a..9c79f96931a0b2a05d98191ea8eb31a3a01818fc 100644
index 24ca200ac662028d45180b21c3d79f2a4b96636e..b35025f7a06cae964858452c8f9e96655e34c47a 100644
--- a/chrome/browser/pdf/pdf_extension_util.cc
+++ b/chrome/browser/pdf/pdf_extension_util.cc
@@ -259,10 +259,13 @@ bool IsPrintingEnabled(content::BrowserContext* context) {

View File

@@ -313,7 +313,7 @@ index 18f283e625101318ee14b50e6e765dfd1c9a1a44..44a3a55974c9e4b9e715574075f25661
auto DrawAsSinglePath = [&]() {
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index d5fe1468676285540909ee078d5b88a9bd8e197c..427c77b17d3e130c43a69d79d330bf5c4759f4cc 100644
index 80b0abeb7f3c18019aca2f431240a84c6dd749d7..0ebf7f1be37e0b0fccb594e83ecaacc3d4c6f510 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -214,6 +214,10 @@

View File

@@ -0,0 +1,81 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sattard@anthropic.com>
Date: Mon, 30 Mar 2026 03:05:40 -0700
Subject: fix: handle embedder windows shown after WebContentsViewCocoa attach
The occlusion checker assumes windows are shown before or at the same
time as a WebContentsViewCocoa is attached. Embedders like Electron
support creating a window hidden, attaching web contents, and showing
later. This breaks three assumptions:
1. updateWebContentsVisibility only checks -[NSWindow isOccluded], which
defaults to NO for never-shown windows, so viewDidMoveToWindow
incorrectly reports kVisible for hidden windows.
2. windowChangedOcclusionState: only responds to checker-originated
notifications, but setOccluded: early-returns when isOccluded doesn't
change. A hidden window's isOccluded is NO and stays NO after show(),
so no checker notification fires on show and the view never updates
to kVisible.
3. performOcclusionStateUpdates iterates orderedWindows and marks
not-yet-shown windows as occluded (their occlusionState lacks the
Visible bit), which stops painting before first show.
Fix by also checking occlusionState in updateWebContentsVisibility,
responding to macOS-originated notifications in
windowChangedOcclusionState:, and skipping non-visible windows in
performOcclusionStateUpdates.
This patch can be removed if the changes are upstreamed to Chromium.
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..54aaffde30c14a27068f89b6de6123abd6ea0660 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
@@ -400,9 +400,11 @@ - (void)performOcclusionStateUpdates {
for (NSWindow* window in windowsFromFrontToBack) {
// The fullscreen transition causes spurious occlusion notifications.
// See https://crbug.com/1081229 . Also, ignore windows that don't have
- // web contentses.
+ // web contentses, and windows that aren't visible (embedders like
+ // Electron may create windows hidden with web contents already attached;
+ // marking these as occluded would stop painting before first show).
if (window == _windowReceivingFullscreenTransitionNotifications ||
- ![window containsWebContentsViewCocoa])
+ ![window isVisible] || ![window containsWebContentsViewCocoa])
continue;
[window setOccluded:[self isWindowOccluded:window
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
index 1ef2c9052262eccdbc40030746a858b7f30ac469..34708d45274f95b5f35cdefad98ad4a1c3c28e1c 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -477,7 +477,8 @@ - (void)updateWebContentsVisibility {
Visibility visibility = Visibility::kVisible;
if ([self isHiddenOrHasHiddenAncestor] || ![self window])
visibility = Visibility::kHidden;
- else if ([[self window] isOccluded])
+ else if ([[self window] isOccluded] ||
+ !([[self window] occlusionState] & NSWindowOcclusionStateVisible))
visibility = Visibility::kOccluded;
[self updateWebContentsVisibility:visibility];
@@ -521,11 +522,12 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
}
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
- // Only respond to occlusion notifications sent by the occlusion checker.
- NSDictionary* userInfo = [aNotification userInfo];
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
- if (userInfo[occlusionCheckerKey] != nil)
- [self updateWebContentsVisibility];
+ // Respond to occlusion notifications from both macOS and the occlusion
+ // checker. Embedders (e.g. Electron) may attach a WebContentsViewCocoa to
+ // a window that has not yet been shown; macOS will notify us when the
+ // window's occlusion state changes, but the occlusion checker will not
+ // because -[NSWindow isOccluded] remains NO before and after show.
+ [self updateWebContentsVisibility];
}
- (void)viewDidMoveToWindow {

View File

@@ -1,279 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: David Sanders <dsanders11@ucsbalum.com>
Date: Wed, 8 Jan 2025 23:53:27 -0800
Subject: Revert "Code Health: Clean up stale MacWebContentsOcclusion"
Chrome has removed this WebContentsOcclusion feature flag upstream,
which is now causing our visibility tests to break. This patch
restores the legacy occlusion behavior to ensure the roll can continue
while we debug the issue.
This patch can be removed when the root cause because the visibility
specs failing on MacOS only is debugged and fixed. It should be removed
before Electron 35's stable date.
Refs: https://chromium-review.googlesource.com/c/chromium/src/+/6078344
This partially (leaves the removal of the feature flag) reverts
ef865130abd5539e7bce12308659b19980368f12.
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
index 04c7635cc093d9d676869383670a8f2199f14ac6..52d76e804e47ab0b56016d26262d6d67cbc00875 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
@@ -11,6 +11,8 @@
#include "base/metrics/field_trial_params.h"
#import "content/app_shim_remote_cocoa/web_contents_view_cocoa.h"
+extern CONTENT_EXPORT const base::FeatureParam<bool>
+ kEnhancedWindowOcclusionDetection;
extern CONTENT_EXPORT const base::FeatureParam<bool>
kDisplaySleepAndAppHideDetection;
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..0a2dba6aa2d48bc39d2a55c8b4d6606744c10ca7 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
@@ -14,9 +14,16 @@
#include "base/mac/mac_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
+#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
+using features::kMacWebContentsOcclusion;
+
+// Experiment features.
+const base::FeatureParam<bool> kEnhancedWindowOcclusionDetection{
+ &kMacWebContentsOcclusion, "EnhancedWindowOcclusionDetection", false};
+
namespace {
NSString* const kWindowDidChangePositionInWindowList =
@@ -125,7 +132,8 @@ - (void)dealloc {
- (BOOL)isManualOcclusionDetectionEnabled {
return [WebContentsOcclusionCheckerMac
- manualOcclusionDetectionSupportedForCurrentMacOSVersion];
+ manualOcclusionDetectionSupportedForCurrentMacOSVersion] &&
+ kEnhancedWindowOcclusionDetection.Get();
}
// Alternative implementation of orderWindow:relativeTo:. Replaces
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
index 1ef2c9052262eccdbc40030746a858b7f30ac469..c7101b0d71826b05f61bfe0e74429d922769e792 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -15,6 +15,7 @@
#import "content/app_shim_remote_cocoa/web_drag_source_mac.h"
#import "content/browser/web_contents/web_contents_view_mac.h"
#import "content/browser/web_contents/web_drag_dest_mac.h"
+#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "ui/base/clipboard/clipboard_constants.h"
@@ -27,6 +28,7 @@
#include "ui/resources/grit/ui_resources.h"
using content::DropData;
+using features::kMacWebContentsOcclusion;
using remote_cocoa::mojom::DraggingInfo;
using remote_cocoa::mojom::SelectionDirection;
@@ -122,12 +124,15 @@ @implementation WebContentsViewCocoa {
WebDragSource* __strong _dragSource;
NSDragOperation _dragOperation;
+ BOOL _inFullScreenTransition;
BOOL _willSetWebContentsOccludedAfterDelay;
}
+ (void)initialize {
- // Create the WebContentsOcclusionCheckerMac shared instance.
- [WebContentsOcclusionCheckerMac sharedInstance];
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ // Create the WebContentsOcclusionCheckerMac shared instance.
+ [WebContentsOcclusionCheckerMac sharedInstance];
+ }
}
- (instancetype)initWithViewsHostableView:(ui::ViewsHostableView*)v {
@@ -438,6 +443,7 @@ - (void)updateWebContentsVisibility:
(remote_cocoa::mojom::Visibility)visibility {
using remote_cocoa::mojom::Visibility;
+ DCHECK(base::FeatureList::IsEnabled(kMacWebContentsOcclusion));
if (!_host)
return;
@@ -483,6 +489,20 @@ - (void)updateWebContentsVisibility {
[self updateWebContentsVisibility:visibility];
}
+- (void)legacyUpdateWebContentsVisibility {
+ using remote_cocoa::mojom::Visibility;
+ if (!_host || _inFullScreenTransition)
+ return;
+ Visibility visibility = Visibility::kVisible;
+ if ([self isHiddenOrHasHiddenAncestor] || ![self window])
+ visibility = Visibility::kHidden;
+ else if ([[self window] occlusionState] & NSWindowOcclusionStateVisible)
+ visibility = Visibility::kVisible;
+ else
+ visibility = Visibility::kOccluded;
+ _host->OnWindowVisibilityChanged(visibility);
+}
+
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
// Subviews do not participate in auto layout unless the the size this view
// changes. This allows RenderWidgetHostViewMac::SetBounds(..) to select a
@@ -505,11 +525,39 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
NSWindow* oldWindow = [self window];
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ if (oldWindow) {
+ [notificationCenter
+ removeObserver:self
+ name:NSWindowDidChangeOcclusionStateNotification
+ object:oldWindow];
+ }
+
+ if (newWindow) {
+ [notificationCenter
+ addObserver:self
+ selector:@selector(windowChangedOcclusionState:)
+ name:NSWindowDidChangeOcclusionStateNotification
+ object:newWindow];
+ }
+
+ return;
+ }
+
+ _inFullScreenTransition = NO;
if (oldWindow) {
- [notificationCenter
- removeObserver:self
- name:NSWindowDidChangeOcclusionStateNotification
- object:oldWindow];
+ NSArray* notificationsToRemove = @[
+ NSWindowDidChangeOcclusionStateNotification,
+ NSWindowWillEnterFullScreenNotification,
+ NSWindowDidEnterFullScreenNotification,
+ NSWindowWillExitFullScreenNotification,
+ NSWindowDidExitFullScreenNotification
+ ];
+ for (NSString* notificationName in notificationsToRemove) {
+ [notificationCenter removeObserver:self
+ name:notificationName
+ object:oldWindow];
+ }
}
if (newWindow) {
@@ -517,26 +565,66 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
selector:@selector(windowChangedOcclusionState:)
name:NSWindowDidChangeOcclusionStateNotification
object:newWindow];
+ // The fullscreen transition causes spurious occlusion notifications.
+ // See https://crbug.com/1081229
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionStarted:)
+ name:NSWindowWillEnterFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionComplete:)
+ name:NSWindowDidEnterFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionStarted:)
+ name:NSWindowWillExitFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionComplete:)
+ name:NSWindowDidExitFullScreenNotification
+ object:newWindow];
}
}
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
- // Only respond to occlusion notifications sent by the occlusion checker.
- NSDictionary* userInfo = [aNotification userInfo];
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
- if (userInfo[occlusionCheckerKey] != nil)
- [self updateWebContentsVisibility];
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+}
+
+- (void)fullscreenTransitionStarted:(NSNotification*)notification {
+ _inFullScreenTransition = YES;
+}
+
+- (void)fullscreenTransitionComplete:(NSNotification*)notification {
+ _inFullScreenTransition = NO;
}
- (void)viewDidMoveToWindow {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
- (void)viewDidHide {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
- (void)viewDidUnhide {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
diff --git a/content/common/features.cc b/content/common/features.cc
index ee2fa2cd950a6c5cddc5904ee7a4656a18b9d73d..10a1e1e14777b61f6c42266f6f085bd47b759efd 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -364,6 +364,14 @@ BASE_FEATURE(kInterestGroupUpdateIfOlderThan, base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kIOSurfaceCapturer, base::FEATURE_ENABLED_BY_DEFAULT);
#endif
+// Feature that controls whether WebContentsOcclusionChecker should handle
+// occlusion notifications.
+#if BUILDFLAG(IS_MAC)
+BASE_FEATURE(kMacWebContentsOcclusion,
+ "MacWebContentsOcclusion",
+ base::FEATURE_ENABLED_BY_DEFAULT);
+#endif
+
// When enabled, child process will not terminate itself when IPC is reset.
BASE_FEATURE(kKeepChildProcessAfterIPCReset, base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/content/common/features.h b/content/common/features.h
index 24443780a7196b40096f44826232f77eaab68ffa..9164f2cf39542525ef2c30f572c7d0b557473f5d 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -140,6 +140,9 @@ CONTENT_EXPORT BASE_DECLARE_FEATURE(kInterestGroupUpdateIfOlderThan);
#if BUILDFLAG(IS_MAC)
CONTENT_EXPORT BASE_DECLARE_FEATURE(kIOSurfaceCapturer);
#endif
+#if BUILDFLAG(IS_MAC)
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kMacWebContentsOcclusion);
+#endif
CONTENT_EXPORT BASE_DECLARE_FEATURE(kKeepChildProcessAfterIPCReset);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kLocalNetworkAccessForWorkers);

View File

@@ -533,10 +533,10 @@ index 55a0c986c5b6989ee9ce277bb6a9778abb2ad2ee..809d88f21e5572807e38132d40ee7587
READONLY_PROPERTY(target, "exitCodes", exit_codes);
diff --git a/src/node_file.cc b/src/node_file.cc
index ba6ffc2b6565dea500bc8dd4818c8fcb7648694a..e834325a763f7ea8f53210145b5edd134d6b67e6 100644
index 96aac2d86695732bf6805f2ad2168a62241b5045..547455bb5011677719a8de1f98cb447561bce6aa 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3843,7 +3843,7 @@ void BindingData::Deserialize(Local<Context> context,
@@ -3850,7 +3850,7 @@ void BindingData::Deserialize(Local<Context> context,
int index,
InternalFieldInfoBase* info) {
DCHECK_IS_SNAPSHOT_SLOT(index);
@@ -686,7 +686,7 @@ index d33ee3c26c111e53edf27e6368ca8f64ff30a349..f1c53c44f201b295888e7932c5e3e2b1
Environment* env = Environment::GetCurrent(isolate);
diff --git a/src/node_url.cc b/src/node_url.cc
index 9d1e8ec05161570db11f7b662395509774668d78..9b91f83d879ea02fd3d61913c8dfd35b3bf1ac31 100644
index 9b676a0156ab8ef47f62627be953c23d4fcbf4f4..6294cd03667980e2ad23cae9e7961262369efb62 100644
--- a/src/node_url.cc
+++ b/src/node_url.cc
@@ -70,7 +70,7 @@ void BindingData::Deserialize(Local<Context> context,

View File

@@ -7,7 +7,7 @@ Subject: build: ensure native module compilation fails if not using a new
This should not be upstreamed, it is a quality-of-life patch for downstream module builders.
diff --git a/common.gypi b/common.gypi
index b3b5c23e471ece7584d209b3ae4197c46011d50e..bdcea65ad3e0315c85b1818e695d8b63093aed34 100644
index d9eb9527e3cbb3b101274ab19e6d6ace42f0e022..a1243ad39b8fcf564285ace0b51b1482bd85071b 100644
--- a/common.gypi
+++ b/common.gypi
@@ -89,6 +89,8 @@

View File

@@ -11,7 +11,7 @@ node-gyp will use the result of `process.config` that reflects the environment
in which the binary got built.
diff --git a/common.gypi b/common.gypi
index bdcea65ad3e0315c85b1818e695d8b63093aed34..0653735a0b154e326e5df7049a7beb395f0015c8 100644
index a1243ad39b8fcf564285ace0b51b1482bd85071b..60ac7a50718fd8239fd96b811cdccd1c73b2d606 100644
--- a/common.gypi
+++ b/common.gypi
@@ -128,6 +128,7 @@

View File

@@ -10,7 +10,7 @@ M151, and so we should allow for building until then.
This patch can be removed at the M151 branch point.
diff --git a/common.gypi b/common.gypi
index 0653735a0b154e326e5df7049a7beb395f0015c8..006f52ed18d955da0d9a06e881e86e6e724095ac 100644
index 60ac7a50718fd8239fd96b811cdccd1c73b2d606..709eb83801eeed81f79c4305a86d1a19710298c2 100644
--- a/common.gypi
+++ b/common.gypi
@@ -677,7 +677,7 @@

View File

@@ -7,7 +7,7 @@ common.gypi is a file that's included in the node header bundle, despite
the fact that we do not build node with gyp.
diff --git a/common.gypi b/common.gypi
index c5a7dc9cacf8b983984e7c7de9e63d26e418cc8d..b3b5c23e471ece7584d209b3ae4197c46011d50e 100644
index 283c60eab356a5befc15027cd186ea0416914ee6..d9eb9527e3cbb3b101274ab19e6d6ace42f0e022 100644
--- a/common.gypi
+++ b/common.gypi
@@ -91,6 +91,23 @@

View File

@@ -53,10 +53,10 @@ index 81799fc159cf20344aac64cd7129240deb9a4fe8..12b476ff97603718186dd25b1f435d37
const maybeMain = resolvedOption <= legacyMainResolveExtensionsIndexes.kResolvedByMainIndexNode ?
packageConfig.main || './' : '';
diff --git a/src/node_file.cc b/src/node_file.cc
index 58476306172433db98a3e3a1ab31d13bf42014f1..ba6ffc2b6565dea500bc8dd4818c8fcb7648694a 100644
index c69b4eb461cab79906833152d02f76f81149ad7e..96aac2d86695732bf6805f2ad2168a62241b5045 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3592,13 +3592,25 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
@@ -3599,13 +3599,25 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
}
BindingData::FilePathIsFileReturnType BindingData::FilePathIsFile(
@@ -83,7 +83,7 @@ index 58476306172433db98a3e3a1ab31d13bf42014f1..ba6ffc2b6565dea500bc8dd4818c8fcb
uv_fs_t req;
int rc = uv_fs_stat(env->event_loop(), &req, file_path.c_str(), nullptr);
@@ -3656,6 +3668,11 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
@@ -3663,6 +3675,11 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
std::optional<std::string> initial_file_path;
std::string file_path;
@@ -95,7 +95,7 @@ index 58476306172433db98a3e3a1ab31d13bf42014f1..ba6ffc2b6565dea500bc8dd4818c8fcb
if (args.Length() >= 2 && args[1]->IsString()) {
auto package_config_main = Utf8Value(isolate, args[1]).ToString();
@@ -3676,7 +3693,7 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
@@ -3683,7 +3700,7 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
BufferValue buff_file_path(isolate, local_file_path);
ToNamespacedPath(env, &buff_file_path);
@@ -104,7 +104,7 @@ index 58476306172433db98a3e3a1ab31d13bf42014f1..ba6ffc2b6565dea500bc8dd4818c8fcb
case BindingData::FilePathIsFileReturnType::kIsFile:
return args.GetReturnValue().Set(i);
case BindingData::FilePathIsFileReturnType::kIsNotFile:
@@ -3713,7 +3730,7 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
@@ -3720,7 +3737,7 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo<Value>& args) {
BufferValue buff_file_path(isolate, local_file_path);
ToNamespacedPath(env, &buff_file_path);

View File

@@ -7,10 +7,10 @@ libc++ added [[nodiscard]] to std::filesystem::copy_options operator|=
which causes build failures with -Werror.
diff --git a/src/node_file.cc b/src/node_file.cc
index e834325a763f7ea8f53210145b5edd134d6b67e6..5197202255bcd9345ac19c7fb6106f49c2800770 100644
index 547455bb5011677719a8de1f98cb447561bce6aa..385db5fd6fe5db6bb7ff17e98309b6cd605a82d3 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3453,11 +3453,11 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
@@ -3460,11 +3460,11 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
auto file_copy_opts = std::filesystem::copy_options::recursive;
if (force) {

View File

@@ -6,6 +6,7 @@ Everything here should be project agnostic: it shouldn't rely on project's
structure, or make assumptions about the passed arguments or calls' outcomes.
"""
import hashlib
import io
import os
import posixpath
@@ -18,7 +19,14 @@ sys.path.append(SCRIPT_DIR)
from patches import PATCH_FILENAME_PREFIX, is_patch_location_line
UPSTREAM_HEAD='refs/patches/upstream-head'
# In gclient-new-workdir worktrees, .git/refs is symlinked back to the source
# checkout, so a single fixed ref name would be shared (and clobbered) across
# worktrees. Derive a per-checkout suffix from this script's absolute path so
# each worktree records its own upstream head in the shared refs directory.
_LEGACY_UPSTREAM_HEAD = 'refs/patches/upstream-head'
UPSTREAM_HEAD = (
_LEGACY_UPSTREAM_HEAD + '-' + hashlib.md5(SCRIPT_DIR.encode()).hexdigest()[:8]
)
def is_repo_root(path):
path_exists = os.path.exists(path)
@@ -83,6 +91,8 @@ def import_patches(repo, ref=UPSTREAM_HEAD, **kwargs):
"""same as am(), but we save the upstream HEAD so we can refer to it when we
later export patches"""
update_ref(repo=repo, ref=ref, newvalue='HEAD')
if ref != _LEGACY_UPSTREAM_HEAD:
update_ref(repo=repo, ref=_LEGACY_UPSTREAM_HEAD, newvalue='HEAD')
am(repo=repo, **kwargs)
@@ -102,19 +112,21 @@ def get_commit_count(repo, commit_range):
def guess_base_commit(repo, ref):
"""Guess which commit the patches might be based on"""
try:
upstream_head = get_commit_for_ref(repo, ref)
num_commits = get_commit_count(repo, upstream_head + '..')
return [upstream_head, num_commits]
except subprocess.CalledProcessError:
args = [
'git',
'-C',
repo,
'describe',
'--tags',
]
return subprocess.check_output(args).decode('utf-8').rsplit('-', 2)[0:2]
for candidate in (ref, _LEGACY_UPSTREAM_HEAD):
try:
upstream_head = get_commit_for_ref(repo, candidate)
num_commits = get_commit_count(repo, upstream_head + '..')
return [upstream_head, num_commits]
except subprocess.CalledProcessError:
continue
args = [
'git',
'-C',
repo,
'describe',
'--tags',
]
return subprocess.check_output(args).decode('utf-8').rsplit('-', 2)[0:2]
def format_patch(repo, since):

View File

@@ -308,6 +308,7 @@ void Menu::OnMenuWillClose() {
}
void Menu::OnMenuWillShow() {
keep_alive_ = this;
Emit("menu-will-show");
}

View File

@@ -140,7 +140,7 @@ class Menu : public gin::Wrappable<Menu>,
bool IsVisibleAt(int index) const;
bool WorksWhenHiddenAt(int index) const;
gin_helper::SelfKeepAlive<Menu> keep_alive_{this};
gin_helper::SelfKeepAlive<Menu> keep_alive_{nullptr};
};
} // namespace electron::api

View File

@@ -3126,16 +3126,18 @@ void OnGetDeviceNameToUse(base::WeakPtr<content::WebContents> web_contents,
.Set(printing::kSettingMediaSizeIsDefault, true);
};
const bool use_default_size =
print_settings.FindBool(kUseDefaultPrinterPageSize).value_or(false);
std::optional<gfx::Size> paper_size;
if (use_default_size)
paper_size = GetPrinterDefaultPaperSize(base::UTF16ToUTF8(info.second));
if (!print_settings.Find(printing::kSettingMediaSize)) {
const bool use_default_size =
print_settings.FindBool(kUseDefaultPrinterPageSize).value_or(false);
std::optional<gfx::Size> paper_size;
if (use_default_size)
paper_size = GetPrinterDefaultPaperSize(base::UTF16ToUTF8(info.second));
print_settings.Set(
printing::kSettingMediaSize,
paper_size ? make_media_size(paper_size->height(), paper_size->width())
: make_media_size(297000, 210000));
print_settings.Set(
printing::kSettingMediaSize,
paper_size ? make_media_size(paper_size->height(), paper_size->width())
: make_media_size(297000, 210000));
}
content::RenderFrameHost* rfh = GetRenderFrameHostToUse(web_contents.get());
if (!rfh)

View File

@@ -87,13 +87,6 @@ void InitializeFeatureList() {
std::string(",") + sandbox::policy::features::kNetworkServiceSandbox.name;
#endif
#if BUILDFLAG(IS_MAC)
disable_features +=
// MacWebContentsOcclusion is causing some odd visibility
// issues with multiple web contents
std::string(",") + features::kMacWebContentsOcclusion.name;
#endif
#if BUILDFLAG(ENABLE_PDF_VIEWER)
// Enable window.showSaveFilePicker api for saving pdf files.
// Refs https://issues.chromium.org/issues/373852607

View File

@@ -136,24 +136,10 @@ NativeWindow::~NativeWindow() {
void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
// Setup window from options.
if (int x, y; options.Get(options::kX, &x) && options.Get(options::kY, &y)) {
SetPosition(gfx::Point{x, y});
#if BUILDFLAG(IS_WIN)
// FIXME(felixrieseberg): Dirty, dirty workaround for
// https://github.com/electron/electron/issues/10862
// Somehow, we need to call `SetBounds` twice to get
// usable results. The root cause is still unknown.
SetPosition(gfx::Point{x, y});
#endif
} else if (bool center; options.Get(options::kCenter, &center) && center) {
Center();
}
const bool use_content_size =
options.ValueOrDefault(options::kUseContentSize, false);
// On Linux and Window we may already have maximum size defined.
// On Linux and Windows we may already have minimum and maximum size defined.
extensions::SizeConstraints size_constraints(
use_content_size ? GetContentSizeConstraints() : GetSizeConstraints());
@@ -180,10 +166,32 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
size_constraints.set_maximum_size(gfx::Size(max_width, max_height));
if (use_content_size) {
gfx::Size clamped = size_constraints.ClampSize(GetContentSize());
if (clamped != GetContentSize()) {
SetContentSize(clamped);
}
SetContentSizeConstraints(size_constraints);
} else {
gfx::Size clamped = size_constraints.ClampSize(GetSize());
if (clamped != GetSize()) {
SetSize(clamped);
}
SetSizeConstraints(size_constraints);
}
if (int x, y; options.Get(options::kX, &x) && options.Get(options::kY, &y)) {
SetPosition(gfx::Point{x, y});
#if BUILDFLAG(IS_WIN)
// FIXME(felixrieseberg): Dirty, dirty workaround for
// https://github.com/electron/electron/issues/10862
// Somehow, we need to call `SetBounds` twice to get
// usable results. The root cause is still unknown.
SetPosition(gfx::Point{x, y});
#endif
} else if (bool center; options.Get(options::kCenter, &center) && center) {
Center();
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
if (bool val; options.Get(options::kClosable, &val))
SetClosable(val);

View File

@@ -43,6 +43,14 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
#pragma mark - NSWindowDelegate
- (void)windowDidChangeOcclusionState:(NSNotification*)notification {
// Chromium's WebContentsOcclusionCheckerMac posts synthetic occlusion
// notifications tagged with its class name in userInfo. These reflect the
// checker's manual frame-intersection heuristic, not an actual macOS
// occlusion state change, so the real occlusionState hasn't changed and
// emitting show/hide in response would be spurious.
if (notification.userInfo[@"WebContentsOcclusionCheckerMac"] != nil)
return;
// notification.object is the window that changed its state.
// It's safe to use self.window instead if you don't assign one delegate to
// many windows
@@ -150,45 +158,43 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
windowSize.width() - contentSize.width() + extraSize.width();
double extraHeightPlusFrame = titleBarHeight + extraSize.height();
newSize.width =
roundf((frameSize.height - extraHeightPlusFrame) * aspectRatio +
extraWidthPlusFrame);
newSize.height =
roundf((newSize.width - extraWidthPlusFrame) / aspectRatio +
extraHeightPlusFrame);
auto widthForHeight = [&](double h) {
return (h - extraHeightPlusFrame) * aspectRatio + extraWidthPlusFrame;
};
auto heightForWidth = [&](double w) {
return (w - extraWidthPlusFrame) / aspectRatio + extraHeightPlusFrame;
};
newSize.width = roundf(widthForHeight(frameSize.height));
newSize.height = roundf(heightForWidth(newSize.width));
// Clamp to minimum width/height while ensuring aspect ratio remains.
NSSize minSize = [window minSize];
NSSize zeroSize =
shell_->has_frame() ? NSMakeSize(0, titleBarHeight) : NSZeroSize;
if (!NSEqualSizes(minSize, zeroSize)) {
double minWidthForAspectRatio =
(minSize.height - titleBarHeight) * aspectRatio;
bool atMinHeight =
minSize.height > zeroSize.height && newSize.height <= minSize.height;
newSize.width = atMinHeight ? minWidthForAspectRatio
newSize.width = atMinHeight ? widthForHeight(minSize.height)
: std::max(newSize.width, minSize.width);
double minHeightForAspectRatio = minSize.width / aspectRatio;
bool atMinWidth =
minSize.width > zeroSize.width && newSize.width <= minSize.width;
newSize.height = atMinWidth ? minHeightForAspectRatio
newSize.height = atMinWidth ? heightForWidth(minSize.width)
: std::max(newSize.height, minSize.height);
}
// Clamp to maximum width/height while ensuring aspect ratio remains.
NSSize maxSize = [window maxSize];
if (!NSEqualSizes(maxSize, NSMakeSize(FLT_MAX, FLT_MAX))) {
double maxWidthForAspectRatio = maxSize.height * aspectRatio;
bool atMaxHeight =
maxSize.height < FLT_MAX && newSize.height >= maxSize.height;
newSize.width = atMaxHeight ? maxWidthForAspectRatio
newSize.width = atMaxHeight ? widthForHeight(maxSize.height)
: std::min(newSize.width, maxSize.width);
double maxHeightForAspectRatio = maxSize.width / aspectRatio;
bool atMaxWidth =
maxSize.width < FLT_MAX && newSize.width >= maxSize.width;
newSize.height = atMaxWidth ? maxHeightForAspectRatio
newSize.height = atMaxWidth ? heightForWidth(maxSize.width)
: std::min(newSize.height, maxSize.height);
}
}

View File

@@ -402,7 +402,7 @@ void InspectableWebContents::SetDockState(const std::string& state) {
can_dock_ = false;
} else {
can_dock_ = true;
dock_state_ = IsValidDockState(state) ? state : "right";
dock_state_ = (state.empty() || IsValidDockState(state)) ? state : "right";
}
}

View File

@@ -1671,6 +1671,32 @@ describe('BrowserWindow module', () => {
expectBoundsEqual(w.getMaximumSize(), [900, 600]);
});
it('creates window at min size when a smaller size is requested', () => {
const w1 = new BrowserWindow({
show: false,
width: 200,
height: 200,
minWidth: 300,
minHeight: 300
});
const size = w1.getSize();
expect(size[0]).to.equal(300);
expect(size[1]).to.equal(300);
});
it('creates window at max size when a larger size is requested', () => {
const w1 = new BrowserWindow({
show: false,
width: 300,
height: 300,
maxWidth: 200,
maxHeight: 200
});
const size = w1.getSize();
expect(size[0]).to.equal(200);
expect(size[1]).to.equal(200);
});
it('enforces minimum size', async () => {
w.setMinimumSize(300, 300);
const resize = once(w, 'resize');

View File

@@ -3,6 +3,7 @@ import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen }
import { expect } from 'chai';
import { once } from 'node:events';
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
@@ -309,6 +310,94 @@ describe('WebContentsView', () => {
}
expect(visibilityState).to.equal('visible');
});
it('tracks visibility for multiple child WebContentsViews', async () => {
const w = new BaseWindow({ show: false });
const cv = new View();
w.setContentView(cv);
const v1 = new WebContentsView();
const v2 = new WebContentsView();
cv.addChildView(v1);
cv.addChildView(v2);
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
});
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async () => {
const w = new BaseWindow();
const cv = new View();
w.setContentView(cv);
const v1 = new WebContentsView();
const v2 = new WebContentsView();
cv.addChildView(v1);
cv.addChildView(v2);
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
v1.setVisible(false);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
// v2 should remain visible while v1 is hidden
expect(await v2.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
v1.setVisible(true);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
});
it('fires a single visibilitychange event per show/hide transition', async () => {
const w = new BaseWindow({ show: false });
const v = new WebContentsView();
w.setContentView(v);
await v.webContents.loadURL('about:blank');
await v.webContents.executeJavaScript(`
window.__visChanges = [];
document.addEventListener('visibilitychange', () => {
window.__visChanges.push(document.visibilityState);
});
`);
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
// Give any delayed/queued occlusion updates time to fire.
await setTimeoutAsync(1500);
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v, 'hidden'))).to.eventually.be.fulfilled();
await setTimeoutAsync(1500);
const changes = await v.webContents.executeJavaScript('window.__visChanges');
// Expect exactly one 'visible' followed by one 'hidden'. Extra events
// would indicate the occlusion checker is causing spurious transitions.
expect(changes).to.deep.equal(['visible', 'hidden']);
});
});
describe('setBorderRadius', () => {