mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78896775d9 | ||
|
|
40eb41656a | ||
|
|
5a69e80cac | ||
|
|
90decd4eaf | ||
|
|
ba551d265c | ||
|
|
24784ed024 | ||
|
|
f49f6b1a29 | ||
|
|
c63e0d8b96 | ||
|
|
33a81b40c2 | ||
|
|
eb49ed962d | ||
|
|
7e36ac67ce | ||
|
|
cbae32aac6 | ||
|
|
880b1e08e7 | ||
|
|
aedea576da | ||
|
|
707541d9b2 | ||
|
|
3dcb641a99 | ||
|
|
878a763344 | ||
|
|
6a8d187105 | ||
|
|
29622930a0 | ||
|
|
8b9e721047 | ||
|
|
43bb93908c | ||
|
|
b0055e0500 | ||
|
|
9a7381a328 | ||
|
|
af3e0fca24 | ||
|
|
99d879b52e | ||
|
|
3d8105ae7f | ||
|
|
aba01d38dc | ||
|
|
a0f01336a3 | ||
|
|
4a98b4e27e |
7
.github/actions/build-electron/action.yml
vendored
7
.github/actions/build-electron/action.yml
vendored
@@ -125,6 +125,9 @@ runs:
|
||||
fi
|
||||
sed $SEDOPTION '/.*builtins-pgo/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--turbo-profiling-input/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--reorder-builtins/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--warn-about-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--abort-on-bad-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
cd out/Default
|
||||
@@ -271,12 +274,12 @@ runs:
|
||||
run: ./src/electron/script/actions/move-artifacts.sh
|
||||
- name: Upload Generated Artifacts ${{ inputs.step-suffix }}
|
||||
if: always() && !cancelled()
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: generated_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./generated_artifacts_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
- name: Upload Src Artifacts ${{ inputs.step-suffix }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: src_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src_artifacts_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
|
||||
2
.github/actions/checkout/action.yml
vendored
2
.github/actions/checkout/action.yml
vendored
@@ -43,7 +43,7 @@ runs:
|
||||
curl --unix-socket /var/run/sas/sas.sock --fail "http://foo/$CACHE_FILE?platform=${{ inputs.target-platform }}&getAccountName=true" > sas-token
|
||||
- name: Save SAS Key
|
||||
if: ${{ inputs.generate-sas-token == 'true' }}
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
|
||||
@@ -7,7 +7,7 @@ runs:
|
||||
shell: bash
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(node src/electron/script/yarn.js config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -8,14 +8,14 @@ runs:
|
||||
steps:
|
||||
- name: Obtain SAS Key
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-1
|
||||
enableCrossOsArchive: true
|
||||
- name: Obtain SAS Key
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
@@ -24,7 +24,7 @@ runs:
|
||||
# The cache will always exist here as a result of the checkout job
|
||||
# Either it was uploaded to Azure in the checkout job for this commit
|
||||
# or it was uploaded in the checkout job for a previous commit.
|
||||
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -101,7 +101,7 @@ runs:
|
||||
|
||||
- name: Move Src Cache (Windows)
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
@@ -431,3 +431,30 @@ jobs:
|
||||
- name: GitHub Actions Jobs Done
|
||||
run: |
|
||||
echo "All GitHub Actions Jobs are done"
|
||||
|
||||
check-signed-commits:
|
||||
name: Check signed commits in green PR
|
||||
needs: gha-done
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Remove needs-signed-commits label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --remove-label needs-signed-commits
|
||||
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
if: always() && !cancelled()
|
||||
- name: Upload Test Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}
|
||||
path: src/electron/spec/artifacts
|
||||
|
||||
35
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
35
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Pull Request Opened/Synchronized
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-signed-commits:
|
||||
name: Check signed commits in PR
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Add needs-signed-commits label
|
||||
if: ${{ failure() }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --add-label needs-signed-commits
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ spec/.hash
|
||||
|
||||
# Generated native addon files
|
||||
/spec/fixtures/native-addon/echo/build/
|
||||
/spec/fixtures/native-addon/dialog-helper/build/
|
||||
|
||||
# If someone runs tsc this is where stuff will end up
|
||||
ts-gen
|
||||
|
||||
@@ -9,4 +9,8 @@ npmMinimalAgeGate: 10080
|
||||
npmPreapprovedPackages:
|
||||
- "@electron/*"
|
||||
|
||||
httpProxy: "${HTTP_PROXY:-}"
|
||||
|
||||
httpsProxy: "${HTTPS_PROXY:-}"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
|
||||
2
DEPS
2
DEPS
@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
|
||||
|
||||
vars = {
|
||||
'chromium_version':
|
||||
'146.0.7680.80',
|
||||
'146.0.7680.166',
|
||||
'node_version':
|
||||
'v24.14.0',
|
||||
'nan_version':
|
||||
|
||||
@@ -51,9 +51,6 @@ is_cfi = false
|
||||
use_qt5 = false
|
||||
use_qt6 = false
|
||||
|
||||
# Disables the builtins PGO for V8
|
||||
v8_builtins_profiling_log_file = ""
|
||||
|
||||
# https://chromium.googlesource.com/chromium/src/+/main/docs/dangling_ptr.md
|
||||
# TODO(vertedinde): hunt down dangling pointers on Linux
|
||||
enable_dangling_raw_ptr_checks = false
|
||||
|
||||
@@ -245,6 +245,10 @@ static_library("chrome") {
|
||||
"//chrome/browser/ui/views/dark_mode_manager_linux.cc",
|
||||
"//chrome/browser/ui/views/dark_mode_manager_linux.h",
|
||||
]
|
||||
sources += [
|
||||
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.cc",
|
||||
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h",
|
||||
]
|
||||
public_deps += [ "//components/dbus" ]
|
||||
}
|
||||
|
||||
|
||||
@@ -84,3 +84,7 @@ Currently, Windows high contrast is the only system setting that triggers forced
|
||||
### `nativeTheme.prefersReducedTransparency` _Readonly_
|
||||
|
||||
A `boolean` that indicates whether the user has chosen via system accessibility settings to reduce transparency at the OS level.
|
||||
|
||||
### `nativeTheme.shouldDifferentiateWithoutColor` _macOS_ _Readonly_
|
||||
|
||||
A `boolean` that indicates whether the user prefers UI that differentiates items using something other than color alone (e.g. shapes or labels). This maps to [NSWorkspace.accessibilityDisplayShouldDifferentiateWithoutColor](https://developer.apple.com/documentation/appkit/nsworkspace/accessibilitydisplayshoulddifferentiatewithoutcolor).
|
||||
|
||||
@@ -42,11 +42,15 @@ Returns `boolean` - Whether or not desktop notifications are supported on the cu
|
||||
* `timeoutType` string (optional) _Linux_ _Windows_ - The timeout duration of the notification. Can be 'default' or 'never'.
|
||||
* `replyPlaceholder` string (optional) _macOS_ - The placeholder to write in the inline reply input field.
|
||||
* `sound` string (optional) _macOS_ - The name of the sound file to play when the notification is shown.
|
||||
* `urgency` string (optional) _Linux_ - The urgency level of the notification. Can be 'normal', 'critical', or 'low'.
|
||||
* `urgency` string (optional) _Linux_ _Windows_ - The urgency level of the notification. Can be 'normal', 'critical', or 'low'.
|
||||
* `actions` [NotificationAction[]](structures/notification-action.md) (optional) _macOS_ - Actions to add to the notification. Please read the available actions and limitations in the `NotificationAction` documentation.
|
||||
* `closeButtonText` string (optional) _macOS_ - A custom title for the close button of an alert. An empty string will cause the default localized text to be used.
|
||||
* `toastXml` string (optional) _Windows_ - A custom description of the Notification on Windows superseding all properties above. Provides full customization of design and behavior of the notification.
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, `urgency` type 'critical' sorts the notification higher in Action Center (above default priority notifications), but does not prevent auto-dismissal. To prevent auto-dismissal, you should also set
|
||||
> `timeoutType` to 'never'.
|
||||
|
||||
### Instance Events
|
||||
|
||||
Objects created with `new Notification` emit the following events:
|
||||
|
||||
@@ -56,6 +56,15 @@ app.whenReady().then(() => {
|
||||
})
|
||||
```
|
||||
|
||||
## Protocol names
|
||||
|
||||
[RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) defines what a valid
|
||||
protocol name is:
|
||||
|
||||
> Scheme names consist of a sequence of characters beginning with a letter and followed
|
||||
> by any combination of letters, digits, plus ("+"), period ("."), or hyphen ("-").
|
||||
> Although schemes are case-insensitive, the canonical form is lowercase […].
|
||||
|
||||
## Methods
|
||||
|
||||
The `protocol` module has the following methods:
|
||||
|
||||
@@ -146,13 +146,15 @@ The extra privileges granted to the `file://` protocol by this fuse are incomple
|
||||
The `wasmTrapHandlers` fuse controls whether V8 will use signal handlers to trap Out of Bounds memory
|
||||
access from WebAssembly. The feature works by surrounding the WebAssembly memory with large guard regions
|
||||
and then installing a signal handler that traps attempt to access memory in the guard region. The feature
|
||||
is only supported on the following 64-bit systems.
|
||||
is only supported on the following 64-bit systems:
|
||||
|
||||
Linux. MacOS, Windows - x86_64
|
||||
Linux, MacOS - aarch64
|
||||
* Linux, macOS, Windows - x86_64
|
||||
* Linux, macOS - aarch64
|
||||
|
||||
```text
|
||||
| Guard Pages | WASM heap | Guard Pages |
|
||||
|-----8GB-----| |-----8GB-----|
|
||||
```
|
||||
|
||||
When the fuse is disabled V8 will use explicit bound checks in the generated WebAssembly code to ensure
|
||||
memory safety. However, this method has some downsides
|
||||
|
||||
@@ -782,8 +782,7 @@ WebContents.prototype._init = function () {
|
||||
const originCounts = new Map<string, number>();
|
||||
const openDialogs = new Set<AbortController>();
|
||||
this.on('-run-dialog', async (info, callback) => {
|
||||
const originUrl = new URL(info.frame.url);
|
||||
const origin = originUrl.protocol === 'file:' ? originUrl.href : originUrl.origin;
|
||||
const origin = info.frame.origin === 'file://' ? info.frame.url : info.frame.origin;
|
||||
if ((originCounts.get(origin) ?? 0) < 0) return callback(false, '');
|
||||
|
||||
const prefs = this.getLastWebPreferences();
|
||||
|
||||
@@ -17,11 +17,6 @@ export type WindowOpenArgs = {
|
||||
features: string,
|
||||
}
|
||||
|
||||
const frameNamesToWindow = new Map<string, WebContents>();
|
||||
const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
|
||||
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
|
||||
const getGuestWebContentsByFrameName = (name: string) => frameNamesToWindow.get(name);
|
||||
|
||||
/**
|
||||
* `openGuestWindow` is called to create and setup event handling for the new
|
||||
* window.
|
||||
@@ -47,20 +42,6 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
...overrideBrowserWindowOptions
|
||||
};
|
||||
|
||||
// To spec, subsequent window.open calls with the same frame name (`target` in
|
||||
// spec parlance) will reuse the previous window.
|
||||
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
|
||||
const existingWebContents = getGuestWebContentsByFrameName(frameName);
|
||||
if (existingWebContents) {
|
||||
if (existingWebContents.isDestroyed()) {
|
||||
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
||||
unregisterFrameName(frameName);
|
||||
} else {
|
||||
existingWebContents.loadURL(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (createWindow) {
|
||||
const webContents = createWindow({
|
||||
webContents: guest,
|
||||
@@ -72,7 +53,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest, outlivesOpener });
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -96,7 +77,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
});
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest: window.webContents, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest: window.webContents, outlivesOpener });
|
||||
|
||||
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
|
||||
}
|
||||
@@ -107,10 +88,9 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
* too is the guest destroyed; this is Electron convention and isn't based in
|
||||
* browser behavior.
|
||||
*/
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, outlivesOpener }: {
|
||||
embedder: WebContents,
|
||||
guest: WebContents,
|
||||
frameName: string,
|
||||
outlivesOpener: boolean
|
||||
}) {
|
||||
const closedByEmbedder = function () {
|
||||
@@ -128,13 +108,6 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
|
||||
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
||||
}
|
||||
guest.once('destroyed', closedByUser);
|
||||
|
||||
if (frameName) {
|
||||
registerFrameNameToGuestWindow(frameName, guest);
|
||||
guest.once('destroyed', function () {
|
||||
unregisterFrameName(frameName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Security options that child windows will always inherit from parent windows
|
||||
|
||||
@@ -147,3 +147,4 @@ fix_update_dbus_signal_signature_for_xdg_globalshortcuts_portal.patch
|
||||
fix_set_correct_app_id_on_linux.patch
|
||||
fix_pass_trigger_for_global_shortcuts_on_wayland.patch
|
||||
feat_plumb_node_integration_in_worker_through_workersettings.patch
|
||||
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch
|
||||
|
||||
@@ -33,10 +33,10 @@ index 4a742db71f62f9ac891ceeb0604ca0b99d1d89c1..2c5af6482e2b6905552a05b16d3df0a4
|
||||
"//base",
|
||||
"//build:branding_buildflags",
|
||||
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
|
||||
index 2fc3a991d89093ff9139eb09d74123197155caff..0862aa96c2a7b496338ac0593f84fcfa21f25572 100644
|
||||
index a2a14349d40ce34831ab063cd5eb55cd5085c814..1a861ff7867f19935178c8368a9a720230fee026 100644
|
||||
--- a/chrome/browser/BUILD.gn
|
||||
+++ b/chrome/browser/BUILD.gn
|
||||
@@ -4749,7 +4749,7 @@ static_library("browser") {
|
||||
@@ -4751,7 +4751,7 @@ static_library("browser") {
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ index 2fc3a991d89093ff9139eb09d74123197155caff..0862aa96c2a7b496338ac0593f84fcfa
|
||||
# than here in :chrome_dll.
|
||||
deps += [ "//chrome:packed_resources_integrity_header" ]
|
||||
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
|
||||
index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f564843bcef 100644
|
||||
index 40ea51f97470e2b86f8d2d373ea99a2a71ad185e..db6a2291ce77d89c8e28a1435336fd939e436906 100644
|
||||
--- a/chrome/test/BUILD.gn
|
||||
+++ b/chrome/test/BUILD.gn
|
||||
@@ -7728,9 +7728,12 @@ test("unit_tests") {
|
||||
@@ -7731,9 +7731,12 @@ test("unit_tests") {
|
||||
"//chrome/notification_helper",
|
||||
]
|
||||
|
||||
@@ -63,7 +63,7 @@ index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f56
|
||||
"//chrome//services/util_win:unit_tests",
|
||||
"//chrome/app:chrome_dll_resources",
|
||||
"//chrome/app:win_unit_tests",
|
||||
@@ -8698,6 +8701,10 @@ test("unit_tests") {
|
||||
@@ -8703,6 +8706,10 @@ test("unit_tests") {
|
||||
"../browser/performance_manager/policies/background_tab_loading_policy_unittest.cc",
|
||||
]
|
||||
|
||||
@@ -74,7 +74,7 @@ index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f56
|
||||
sources += [
|
||||
# The importer code is not used on Android.
|
||||
"../common/importer/firefox_importer_utils_unittest.cc",
|
||||
@@ -8755,7 +8762,6 @@ test("unit_tests") {
|
||||
@@ -8760,7 +8767,6 @@ test("unit_tests") {
|
||||
# TODO(crbug.com/417513088): Maybe merge with the non-android `deps` declaration above?
|
||||
deps += [
|
||||
"../browser/screen_ai:screen_ai_install_state",
|
||||
|
||||
@@ -9,10 +9,10 @@ potentially prevent a window from being created.
|
||||
TODO(loc): this patch is currently broken.
|
||||
|
||||
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
index 46368e70af175d8d0ab0fb5a36d258e48270371e..8d7be769a6c76650ae999338578215dcd324c199 100644
|
||||
index 2d8a70f5fc0f6c2dc2a7587b7bc2e43dbcee8f0e..a87bd09d7a12c5f003488792843cd1807ee1e30f 100644
|
||||
--- a/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
@@ -9990,6 +9990,7 @@ void RenderFrameHostImpl::CreateNewWindow(
|
||||
@@ -9997,6 +9997,7 @@ void RenderFrameHostImpl::CreateNewWindow(
|
||||
last_committed_origin_, params->window_container_type,
|
||||
params->target_url, params->referrer.To<Referrer>(),
|
||||
params->frame_name, params->disposition, *params->features,
|
||||
|
||||
@@ -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 70a7e2a5203d3cdddbad7eecca28d65945522fed..35751435ebe8205a5c9d73bed0422ccbe61ab8b4 100644
|
||||
index d5fe1468676285540909ee078d5b88a9bd8e197c..427c77b17d3e130c43a69d79d330bf5c4759f4cc 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 @@
|
||||
|
||||
@@ -28,7 +28,7 @@ The patch should be removed in favor of either:
|
||||
Upstream bug https://bugs.chromium.org/p/chromium/issues/detail?id=1081397.
|
||||
|
||||
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
|
||||
index 5b79df01e0a5ee81919ebed7d689e430fe7fe305..b11808a69483f4cbcc56d90cc6161984df90c1e4 100644
|
||||
index 7d101d40116bf743f940f32ba4c9b507aa9a235b..2aa1584fd451fb15ec6084fb0c19724e6c63e0e3 100644
|
||||
--- a/content/browser/renderer_host/navigation_request.cc
|
||||
+++ b/content/browser/renderer_host/navigation_request.cc
|
||||
@@ -11666,6 +11666,11 @@ url::Origin NavigationRequest::GetOriginForURLLoaderFactoryUnchecked() {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Keeley Hammond <khammond@slack-corp.com>
|
||||
Date: Thu, 19 Mar 2026 00:34:37 -0700
|
||||
Subject: fix: fire MENU_POPUP_START for dynamically created ARIA menus
|
||||
|
||||
When an ARIA menu element is dynamically created (e.g. via appendChild)
|
||||
rather than being shown by toggling visibility, the AXMenuOpened event
|
||||
was not fired. The OnIgnoredChanged path handles the visibility toggle
|
||||
case, but OnAtomicUpdateFinished did not fire MENU_POPUP_START for
|
||||
newly created menu nodes.
|
||||
|
||||
Previous attempts to fix this (crbug.com/1254875) were reverted because
|
||||
they fired the event too eagerly in OnNodeCreated (before the tree was
|
||||
fully formed) and without filtering, causing regressions with screen
|
||||
readers on pages that misused role="menu".
|
||||
|
||||
This fix addresses both issues:
|
||||
1. Fires MENU_POPUP_START in OnAtomicUpdateFinished (after the tree
|
||||
update is complete) rather than in OnNodeCreated.
|
||||
2. Only fires if the menu has at least one menuitem child, filtering
|
||||
out false positives from misused role="menu" elements.
|
||||
|
||||
MENU_POPUP_END for deleted menus is already handled by
|
||||
AXTreeManager::OnNodeWillBeDeleted, which fires the event directly
|
||||
on the menu node before destruction.
|
||||
|
||||
The change is behind the DynamicMenuPopupEvents feature flag, disabled
|
||||
by default, to allow stabilization before enabling by default. Enable
|
||||
with --enable-features=DynamicMenuPopupEvents.
|
||||
|
||||
This patch can be removed when a CL containing the fix is accepted
|
||||
into Chromium.
|
||||
|
||||
Bug: 40794596
|
||||
|
||||
diff --git a/ui/accessibility/ax_event_generator.cc b/ui/accessibility/ax_event_generator.cc
|
||||
index 5e0d7a48b4a039db67b5cc6b7e86103739702b40..517fb5e9904f3907de177e172c76328910bb7333 100644
|
||||
--- a/ui/accessibility/ax_event_generator.cc
|
||||
+++ b/ui/accessibility/ax_event_generator.cc
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "ui/accessibility/ax_event_generator.h"
|
||||
|
||||
+#include "base/feature_list.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "ui/accessibility/ax_enums.mojom.h"
|
||||
#include "ui/accessibility/ax_event.h"
|
||||
@@ -12,6 +13,12 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
+// Feature flag for firing MENU_POPUP_START for dynamically created ARIA menus.
|
||||
+// Disabled by default to allow stabilization before enabling globally.
|
||||
+BASE_FEATURE(kDynamicMenuPopupEvents,
|
||||
+ "DynamicMenuPopupEvents",
|
||||
+ base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
+
|
||||
namespace {
|
||||
|
||||
bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events,
|
||||
@@ -914,12 +921,31 @@ void AXEventGenerator::OnAtomicUpdateFinished(
|
||||
/*new_value*/ true);
|
||||
}
|
||||
|
||||
- if (IsAlert(change.node->GetRole()))
|
||||
+ if (IsAlert(change.node->GetRole())) {
|
||||
AddEvent(change.node, Event::ALERT);
|
||||
- else if (change.node->data().IsActiveLiveRegionRoot())
|
||||
+ } else if (change.node->data().IsActiveLiveRegionRoot()) {
|
||||
AddEvent(change.node, Event::LIVE_REGION_CREATED);
|
||||
- else if (change.node->data().IsContainedInActiveLiveRegion())
|
||||
+ } else if (change.node->data().IsContainedInActiveLiveRegion()) {
|
||||
FireLiveRegionEvents(change.node, /* is_removal */ false);
|
||||
+ }
|
||||
+
|
||||
+ // Fire MENU_POPUP_START when a menu is dynamically created (e.g. via
|
||||
+ // appendChild). The OnIgnoredChanged path handles menus that already exist
|
||||
+ // in the DOM and are shown/hidden. This handles the case where the menu
|
||||
+ // element itself is created on the fly.
|
||||
+ // Only fire if the menu has at least one menuitem child, to avoid false
|
||||
+ // positives from elements that misuse role="menu".
|
||||
+ if (base::FeatureList::IsEnabled(kDynamicMenuPopupEvents) &&
|
||||
+ change.node->GetRole() == ax::mojom::Role::kMenu &&
|
||||
+ !change.node->IsInvisibleOrIgnored()) {
|
||||
+ for (auto iter = change.node->UnignoredChildrenBegin();
|
||||
+ iter != change.node->UnignoredChildrenEnd(); ++iter) {
|
||||
+ if (IsMenuItem(iter->GetRole())) {
|
||||
+ AddEvent(change.node, Event::MENU_POPUP_START);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
FireActiveDescendantEvents();
|
||||
@@ -17,7 +17,7 @@ Revert "Reland "Port net::CookieCryptoDelegate to os_crypt async""
|
||||
This reverts commit f01b115c7e21a09cc762f65bf7fd9c6ea9d9d0f8.
|
||||
|
||||
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
|
||||
index 0862aa96c2a7b496338ac0593f84fcfa21f25572..aed5a316bd3d97df715f779273ae4c283cd29c92 100644
|
||||
index 1a861ff7867f19935178c8368a9a720230fee026..b1ca947122f4ea715be18a0fd4e75b30fffc5a3c 100644
|
||||
--- a/chrome/browser/BUILD.gn
|
||||
+++ b/chrome/browser/BUILD.gn
|
||||
@@ -714,6 +714,8 @@ static_library("browser") {
|
||||
|
||||
@@ -1189,7 +1189,7 @@ index a1068589ad844518038ee7bc15a3de9bc5cba525..1ff781c49f086ec8015c7d3c44567dbe
|
||||
|
||||
} // namespace content
|
||||
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
|
||||
index 8575983261c7b57fc85097edb94a8e6f306974f9..aae50b6830450baf27f2834a8187540d7ff6eb35 100644
|
||||
index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050c3062526 100644
|
||||
--- a/content/test/BUILD.gn
|
||||
+++ b/content/test/BUILD.gn
|
||||
@@ -700,6 +700,7 @@ static_library("test_support") {
|
||||
@@ -1217,7 +1217,7 @@ index 8575983261c7b57fc85097edb94a8e6f306974f9..aae50b6830450baf27f2834a8187540d
|
||||
]
|
||||
|
||||
if (!(is_chromeos && target_cpu == "arm64" && current_cpu == "arm")) {
|
||||
@@ -3411,6 +3415,7 @@ test("content_unittests") {
|
||||
@@ -3412,6 +3416,7 @@ test("content_unittests") {
|
||||
"//ui/shell_dialogs",
|
||||
"//ui/webui:test_support",
|
||||
"//url",
|
||||
|
||||
@@ -10,10 +10,10 @@ on Windows. We should refactor our code so that this patch isn't
|
||||
necessary.
|
||||
|
||||
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
|
||||
index eecdbe8a279ef1a7d9aed4f5496e871d54092e0f..16ff3ffbe8300534cef76f857284ef92ad0a88f6 100644
|
||||
index d17637a54208450504d071a3f10c20668cfbe76d..f3ffc975d794f356d9a83837fd977e758b726501 100644
|
||||
--- a/testing/variations/fieldtrial_testing_config.json
|
||||
+++ b/testing/variations/fieldtrial_testing_config.json
|
||||
@@ -27059,6 +27059,21 @@
|
||||
@@ -27095,6 +27095,21 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@ Note that we also need to manually update embedder's
|
||||
`api::WebContents::IsFullscreenForTabOrPending` value.
|
||||
|
||||
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
index 8d7be769a6c76650ae999338578215dcd324c199..3e8021289c00ec6b15457b17173dfed386eac2fe 100644
|
||||
index a87bd09d7a12c5f003488792843cd1807ee1e30f..b38240fd422163f09bfb8d4b40213a1940a72acd 100644
|
||||
--- a/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
|
||||
@@ -9097,6 +9097,17 @@ void RenderFrameHostImpl::EnterFullscreen(
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
chore_allow_customizing_microtask_policy_per_context.patch
|
||||
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Sun, 22 Mar 2026 10:51:26 +0000
|
||||
Subject: build: warn instead of abort on builtin PGO profile mismatch
|
||||
|
||||
Electron sets v8_enable_javascript_promise_hooks = true to support
|
||||
Node.js async_hooks (see node/src/env.cc SetPromiseHooks usage:
|
||||
https://github.com/nodejs/node/blob/abff716eaccd0c4f4949d1315cb057a45979649d/src/env.cc#L223-L236).
|
||||
This flag adds conditional branches to builtins-microtask-queue-gen.cc
|
||||
and promise-misc.tq, changing the control-flow graph hash of several
|
||||
Promise/async builtins. This invalidates V8's pre-generated PGO profile
|
||||
for those builtins (built with Chrome defaults where the flag is off).
|
||||
|
||||
Rather than disabling builtins PGO entirely, warn and skip mismatched
|
||||
builtins so all other builtins still benefit from PGO.
|
||||
|
||||
diff --git a/BUILD.gn b/BUILD.gn
|
||||
index 15de2179a0e5ce50d5c659a9d15a920c50124c3e..9fb3a69450bdcab42c2571e8b1f57c4f3c283d9a 100644
|
||||
--- a/BUILD.gn
|
||||
+++ b/BUILD.gn
|
||||
@@ -2764,9 +2764,11 @@ template("run_mksnapshot") {
|
||||
"--turbo-profiling-input",
|
||||
rebase_path(v8_builtins_profiling_log_file, root_build_dir),
|
||||
|
||||
- # Replace this with --warn-about-builtin-profile-data to see the full
|
||||
- # list of builtins with incompatible profiles.
|
||||
- "--abort-on-bad-builtin-profile-data",
|
||||
+ # Electron: Use warn instead of abort so that builtins whose control
|
||||
+ # flow is changed by Electron's build flags (e.g. RunMicrotasks via
|
||||
+ # v8_enable_javascript_promise_hooks) are skipped rather than failing
|
||||
+ # the build. All other builtins still receive PGO.
|
||||
+ "--warn-about-builtin-profile-data",
|
||||
]
|
||||
|
||||
if (!v8_enable_builtins_profiling && v8_enable_builtins_reordering) {
|
||||
@@ -32,7 +32,8 @@ async function main () {
|
||||
}));
|
||||
const hitRate = stats.CacheHit / (stats.Remote + stats.CacheHit + stats.LocalFallback);
|
||||
|
||||
console.log(`Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
|
||||
const messagePrefix = process.env.GITHUB_ACTIONS ? '::notice title=Build Stats::' : '';
|
||||
console.log(`${messagePrefix}Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
|
||||
|
||||
if (uploadStats) {
|
||||
if (!process.env.DD_API_KEY) {
|
||||
|
||||
@@ -317,6 +317,12 @@ void BaseWindow::OnWindowSheetEnd() {
|
||||
Emit("sheet-end");
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowIsKeyChanged(bool is_key) {
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
window()->SetActive(is_key);
|
||||
#endif
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowEnterHtmlFullScreen() {
|
||||
Emit("enter-html-full-screen");
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
|
||||
void OnWindowRotateGesture(float rotation) override;
|
||||
void OnWindowSheetBegin() override;
|
||||
void OnWindowSheetEnd() override;
|
||||
void OnWindowIsKeyChanged(bool is_key) override;
|
||||
void OnWindowEnterFullScreen() override;
|
||||
void OnWindowLeaveFullScreen() override;
|
||||
void OnWindowEnterHtmlFullScreen() override;
|
||||
|
||||
@@ -280,16 +280,22 @@ v8::Local<v8::Value> BrowserWindow::GetWebContents(v8::Isolate* isolate) {
|
||||
}
|
||||
|
||||
void BrowserWindow::OnWindowShow() {
|
||||
if (!web_contents_shown_) {
|
||||
web_contents()->WasShown();
|
||||
web_contents_shown_ = true;
|
||||
}
|
||||
BaseWindow::OnWindowShow();
|
||||
}
|
||||
|
||||
void BrowserWindow::OnWindowHide() {
|
||||
web_contents()->WasOccluded();
|
||||
web_contents_shown_ = false;
|
||||
BaseWindow::OnWindowHide();
|
||||
}
|
||||
|
||||
void BrowserWindow::Show() {
|
||||
web_contents()->WasShown();
|
||||
web_contents_shown_ = true;
|
||||
BaseWindow::Show();
|
||||
}
|
||||
|
||||
@@ -298,6 +304,7 @@ void BrowserWindow::ShowInactive() {
|
||||
if (IsModal())
|
||||
return;
|
||||
web_contents()->WasShown();
|
||||
web_contents_shown_ = true;
|
||||
BaseWindow::ShowInactive();
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class BrowserWindow : public BaseWindow,
|
||||
// Helpers.
|
||||
|
||||
v8::Global<v8::Value> web_contents_;
|
||||
bool web_contents_shown_ = false;
|
||||
v8::Global<v8::Value> web_contents_view_;
|
||||
base::WeakPtr<api::WebContents> api_web_contents_;
|
||||
|
||||
|
||||
@@ -151,7 +151,10 @@ void OnTraceBufferUsageAvailable(
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||
float percent_full,
|
||||
size_t approximate_count) {
|
||||
auto dict = gin_helper::Dictionary::CreateEmpty(promise.isolate());
|
||||
v8::Isolate* isolate = promise.isolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
|
||||
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
|
||||
dict.Set("percentage", percent_full);
|
||||
dict.Set("value", approximate_count);
|
||||
|
||||
|
||||
@@ -147,7 +147,12 @@ gin::ObjectTemplateBuilder NativeTheme::GetObjectTemplateBuilder(
|
||||
&NativeTheme::ShouldUseInvertedColorScheme)
|
||||
.SetProperty("inForcedColorsMode", &NativeTheme::InForcedColorsMode)
|
||||
.SetProperty("prefersReducedTransparency",
|
||||
&NativeTheme::GetPrefersReducedTransparency);
|
||||
&NativeTheme::GetPrefersReducedTransparency)
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
.SetProperty("shouldDifferentiateWithoutColor",
|
||||
&NativeTheme::ShouldDifferentiateWithoutColor)
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
const char* NativeTheme::GetTypeName() {
|
||||
|
||||
@@ -56,6 +56,9 @@ class NativeTheme final : public gin_helper::DeprecatedWrappable<NativeTheme>,
|
||||
bool ShouldUseInvertedColorScheme();
|
||||
bool InForcedColorsMode();
|
||||
bool GetPrefersReducedTransparency();
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
bool ShouldDifferentiateWithoutColor();
|
||||
#endif
|
||||
|
||||
// ui::NativeThemeObserver:
|
||||
void OnNativeThemeUpdated(ui::NativeTheme* theme) override;
|
||||
|
||||
@@ -26,4 +26,9 @@ void NativeTheme::UpdateMacOSAppearanceForOverrideValue(
|
||||
[[NSApplication sharedApplication] setAppearance:new_appearance];
|
||||
}
|
||||
|
||||
bool NativeTheme::ShouldDifferentiateWithoutColor() {
|
||||
return [[NSWorkspace sharedWorkspace]
|
||||
accessibilityDisplayShouldDifferentiateWithoutColor];
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -259,7 +259,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
|
||||
EmitWithoutEvent("spawn");
|
||||
}
|
||||
|
||||
void UtilityProcessWrapper::HandleTermination(uint64_t exit_code) {
|
||||
void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
|
||||
// HandleTermination is called from multiple callsites,
|
||||
// we need to ensure we only process it for the first callsite.
|
||||
if (terminated_)
|
||||
@@ -327,7 +327,7 @@ void UtilityProcessWrapper::CloseConnectorPort() {
|
||||
}
|
||||
}
|
||||
|
||||
void UtilityProcessWrapper::Shutdown(uint64_t exit_code) {
|
||||
void UtilityProcessWrapper::Shutdown(uint32_t exit_code) {
|
||||
node_service_remote_.reset();
|
||||
HandleTermination(exit_code);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class UtilityProcessWrapper final
|
||||
static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
|
||||
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
|
||||
|
||||
void Shutdown(uint64_t exit_code);
|
||||
void Shutdown(uint32_t exit_code);
|
||||
|
||||
// gin_helper::Wrappable
|
||||
static gin::DeprecatedWrapperInfo kWrapperInfo;
|
||||
@@ -77,7 +77,7 @@ class UtilityProcessWrapper final
|
||||
void OnServiceProcessLaunch(const base::Process& process);
|
||||
void CloseConnectorPort();
|
||||
|
||||
void HandleTermination(uint64_t exit_code);
|
||||
void HandleTermination(uint32_t exit_code);
|
||||
|
||||
void PostMessage(gin::Arguments* args);
|
||||
bool Kill();
|
||||
|
||||
@@ -127,6 +127,10 @@
|
||||
#include "shell/common/plugin_info.h"
|
||||
#endif // BUILDFLAG(ENABLE_PLUGINS)
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "components/printing/common/print_dialog_linux_factory.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
@@ -415,6 +419,10 @@ void ElectronBrowserMainParts::ToolkitInitialized() {
|
||||
|
||||
ui::LinuxUi::SetInstance(linux_ui);
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
print_dialog_factory_ = std::make_unique<printing::PrintDialogLinuxFactory>();
|
||||
#endif
|
||||
|
||||
// Cursor theme changes are tracked by LinuxUI (via a CursorThemeManager
|
||||
// implementation). Start observing them once it's initialized.
|
||||
ui::CursorFactory::GetInstance()->ObserveThemeChanges();
|
||||
|
||||
@@ -14,8 +14,13 @@
|
||||
#include "content/public/browser/browser_main_parts.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "printing/buildflags/buildflags.h"
|
||||
#include "services/device/public/mojom/geolocation_control.mojom.h"
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "printing/printing_context_linux.h"
|
||||
#endif
|
||||
|
||||
class BrowserProcessImpl;
|
||||
class IconManager;
|
||||
|
||||
@@ -179,6 +184,11 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
std::unique_ptr<display::ScopedNativeScreen> screen_;
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
std::unique_ptr<printing::PrintingContextLinux::PrintDialogFactory>
|
||||
print_dialog_factory_;
|
||||
#endif
|
||||
|
||||
static ElectronBrowserMainParts* self_;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ struct NotificationOptions {
|
||||
std::u16string timeout_type;
|
||||
std::u16string reply_placeholder;
|
||||
std::u16string sound;
|
||||
std::u16string urgency; // Linux
|
||||
std::u16string urgency; // Linux/Windows
|
||||
std::vector<NotificationAction> actions;
|
||||
std::u16string close_button_text;
|
||||
std::u16string toast_xml;
|
||||
|
||||
@@ -72,7 +72,8 @@ std::wstring NotificationPresenterWin::SaveIconToFilesystem(
|
||||
|
||||
std::string filename;
|
||||
if (origin.is_valid()) {
|
||||
filename = base::SHA1HashString(origin.spec()) + ".png";
|
||||
const auto hash = base::SHA1HashString(origin.spec());
|
||||
filename = base::HexEncode(hash) + ".png";
|
||||
} else {
|
||||
const int64_t now_usec = base::Time::Now().since_origin().InMicroseconds();
|
||||
filename = base::NumberToString(now_usec) + ".png";
|
||||
|
||||
@@ -96,6 +96,21 @@ std::wstring GetExecutablePath() {
|
||||
return std::wstring(path, len);
|
||||
}
|
||||
|
||||
// Installers sometimes put the running app in a versioned subfolder and ship a
|
||||
// stub with the same filename one directory up. Point the Start Menu shortcut
|
||||
// at the stub when it exists so toast activation and updates keep a stable
|
||||
// launch path.
|
||||
std::wstring GetShortcutTargetPath(const std::wstring& exe_path) {
|
||||
if (exe_path.empty())
|
||||
return L"";
|
||||
base::FilePath exe_fp(exe_path);
|
||||
base::FilePath stub_candidate =
|
||||
exe_fp.DirName().DirName().Append(exe_fp.BaseName());
|
||||
if (base::PathExists(stub_candidate))
|
||||
return stub_candidate.value();
|
||||
return exe_path;
|
||||
}
|
||||
|
||||
void EnsureCLSIDRegistry() {
|
||||
std::wstring exe = GetExecutablePath();
|
||||
if (exe.empty())
|
||||
@@ -116,7 +131,10 @@ void EnsureCLSIDRegistry() {
|
||||
server_key.WriteValue(nullptr, exe.c_str());
|
||||
}
|
||||
|
||||
bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
|
||||
bool ExistingShortcutValid(const base::FilePath& lnk_path,
|
||||
PCWSTR aumid,
|
||||
const std::wstring& expected_target_path,
|
||||
const std::wstring& expected_working_dir) {
|
||||
if (!base::PathExists(lnk_path))
|
||||
return false;
|
||||
Microsoft::WRL::ComPtr<IShellLink> existing;
|
||||
@@ -128,6 +146,31 @@ bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
|
||||
FAILED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// After an auto-update the .lnk may still have the correct AUMID/CLSID but
|
||||
// point at an old install path; treat that as invalid so we rewrite it.
|
||||
wchar_t target_path[MAX_PATH];
|
||||
if (FAILED(existing->GetPath(target_path, MAX_PATH, nullptr, SLGP_RAWPATH)))
|
||||
return false;
|
||||
if (base::FilePath::CompareIgnoreCase(
|
||||
base::FilePath(expected_target_path).value(),
|
||||
base::FilePath(target_path).value()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t work_dir[MAX_PATH];
|
||||
work_dir[0] = L'\0';
|
||||
if (FAILED(existing->GetWorkingDirectory(work_dir, MAX_PATH)))
|
||||
return false;
|
||||
base::FilePath expected_cwd =
|
||||
base::FilePath(expected_working_dir).NormalizePathSeparators();
|
||||
base::FilePath actual_cwd =
|
||||
base::FilePath(work_dir).NormalizePathSeparators();
|
||||
if (base::FilePath::CompareIgnoreCase(expected_cwd.value(),
|
||||
actual_cwd.value()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IPropertyStore> store;
|
||||
if (FAILED(existing.As(&store)))
|
||||
return false;
|
||||
@@ -157,6 +200,7 @@ void EnsureShortcut() {
|
||||
std::wstring exe = GetExecutablePath();
|
||||
if (exe.empty())
|
||||
return;
|
||||
std::wstring shortcut_target = GetShortcutTargetPath(exe);
|
||||
|
||||
PWSTR programs_path = nullptr;
|
||||
if (FAILED(
|
||||
@@ -195,18 +239,20 @@ void EnsureShortcut() {
|
||||
}
|
||||
}
|
||||
|
||||
if (ExistingShortcutValid(lnk_path, aumid))
|
||||
const std::wstring expected_working_dir =
|
||||
base::FilePath(exe).DirName().value();
|
||||
if (ExistingShortcutValid(lnk_path, aumid, shortcut_target,
|
||||
expected_working_dir))
|
||||
return;
|
||||
|
||||
Microsoft::WRL::ComPtr<IShellLink> shell_link;
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&shell_link))))
|
||||
return;
|
||||
shell_link->SetPath(exe.c_str());
|
||||
shell_link->SetPath(shortcut_target.c_str());
|
||||
shell_link->SetArguments(L"");
|
||||
shell_link->SetDescription(product_name.c_str());
|
||||
shell_link->SetWorkingDirectory(
|
||||
base::FilePath(exe).DirName().value().c_str());
|
||||
shell_link->SetWorkingDirectory(expected_working_dir.c_str());
|
||||
|
||||
Microsoft::WRL::ComPtr<IPropertyStore> prop_store;
|
||||
if (SUCCEEDED(shell_link.As(&prop_store))) {
|
||||
|
||||
@@ -280,8 +280,9 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
|
||||
// Continue to create the toast notification
|
||||
ComPtr<ABI::Windows::UI::Notifications::IToastNotification>
|
||||
toast_notification;
|
||||
if (!CreateToastNotification(toast_xml, notification_id, weak_notification,
|
||||
ui_task_runner, &toast_notification)) {
|
||||
if (!CreateToastNotification(toast_xml, options, notification_id,
|
||||
weak_notification, ui_task_runner,
|
||||
&toast_notification)) {
|
||||
return; // Error already posted to UI thread
|
||||
}
|
||||
|
||||
@@ -349,6 +350,7 @@ bool WindowsToastNotification::CreateToastXmlDocument(
|
||||
// returns the created notification via out parameter.
|
||||
bool WindowsToastNotification::CreateToastNotification(
|
||||
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
|
||||
const NotificationOptions& options,
|
||||
const std::string& notification_id,
|
||||
base::WeakPtr<Notification> weak_notification,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
||||
@@ -416,6 +418,27 @@ bool WindowsToastNotification::CreateToastNotification(
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<winui::Notifications::IToastNotification4> toast4;
|
||||
hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast4));
|
||||
if (SUCCEEDED(hr)) {
|
||||
winui::Notifications::ToastNotificationPriority priority =
|
||||
winui::Notifications::ToastNotificationPriority::
|
||||
ToastNotificationPriority_Default;
|
||||
if (options.urgency == u"critical") {
|
||||
priority = winui::Notifications::ToastNotificationPriority::
|
||||
ToastNotificationPriority_High;
|
||||
}
|
||||
|
||||
hr = toast4->put_Priority(priority);
|
||||
if (FAILED(hr)) {
|
||||
std::string err = base::StrCat({"WinAPI: Setting priority failed, ERROR ",
|
||||
FailureResultToString(hr)});
|
||||
DebugLog(err);
|
||||
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ class WindowsToastNotification : public Notification {
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner);
|
||||
static bool CreateToastNotification(
|
||||
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
|
||||
const NotificationOptions& options,
|
||||
const std::string& notification_id,
|
||||
base::WeakPtr<Notification> weak_notification,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
||||
|
||||
@@ -59,6 +59,8 @@ gfx::Size GetDefaultPrinterDPI(const std::u16string& device_name) {
|
||||
GtkPrintSettings* print_settings = gtk_print_settings_new();
|
||||
int dpi = gtk_print_settings_get_resolution(print_settings);
|
||||
g_object_unref(print_settings);
|
||||
if (dpi <= 0)
|
||||
dpi = printing::kDefaultPdfDpi;
|
||||
return {dpi, dpi};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
|
||||
(electron::NativeWindowMac*)[(id)self.window shell];
|
||||
if (shell && !shell->has_frame())
|
||||
[self cr_mouseDownOnFrameView:event];
|
||||
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
|
||||
- (void)swiz_nsnextstepframe_mouseDown:(NSEvent*)event {
|
||||
@@ -98,8 +98,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
|
||||
if (shell && !shell->has_frame()) {
|
||||
[self cr_mouseDownOnFrameView:event];
|
||||
}
|
||||
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
|
||||
- (void)swiz_nsview_swipeWithEvent:(NSEvent*)event {
|
||||
|
||||
@@ -243,8 +243,8 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
|
||||
// The opaque region is a list of rectangles that contain only fully
|
||||
// opaque pixels of the window. We need to convert the clipping
|
||||
// rounded-rect into this format.
|
||||
SkRRect rrect = layout->GetRoundedWindowContentBounds();
|
||||
gfx::RectF rectf(layout->GetWindowContentBounds());
|
||||
SkRRect rrect = layout->GetRoundedWindowBounds();
|
||||
gfx::RectF rectf(layout->GetWindowBounds());
|
||||
rectf.Scale(scale);
|
||||
// It is acceptable to omit some pixels that are opaque, but the region
|
||||
// must not include any translucent pixels. Therefore, we must
|
||||
|
||||
@@ -112,7 +112,7 @@ ClientFrameViewLinux::~ClientFrameViewLinux() {
|
||||
void ClientFrameViewLinux::Init(NativeWindowViews* window,
|
||||
views::Widget* frame) {
|
||||
FramelessView::Init(window, frame);
|
||||
linux_frame_layout_ = std::make_unique<LinuxCSDFrameLayout>(window);
|
||||
linux_frame_layout_ = std::make_unique<LinuxCSDNativeFrameLayout>(window);
|
||||
|
||||
// Unretained() is safe because the subscription is saved into an instance
|
||||
// member and thus will be cancelled upon the instance's destruction.
|
||||
@@ -156,7 +156,8 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
|
||||
}
|
||||
|
||||
int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
|
||||
return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets());
|
||||
return ResizingBorderHitTestImpl(
|
||||
point, linux_frame_layout_->GetResizeBorderInsets());
|
||||
}
|
||||
|
||||
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
|
||||
@@ -235,8 +236,11 @@ void ClientFrameViewLinux::Layout(PassKey) {
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
|
||||
linux_frame_layout_->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), GetTitlebarBounds(), ShouldPaintAsActive());
|
||||
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
|
||||
frame_provider->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
|
||||
ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
|
||||
}
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::PaintAsActiveChanged() {
|
||||
@@ -267,7 +271,7 @@ void ClientFrameViewLinux::UpdateThemeValues() {
|
||||
}
|
||||
|
||||
theme_values_.window_border_radius =
|
||||
linux_frame_layout_->GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
linux_frame_layout_->GetTopCornerRadiusDip();
|
||||
|
||||
gtk::GtkStyleContextGet(headerbar_context, "min-height",
|
||||
&theme_values_.titlebar_min_height, nullptr);
|
||||
|
||||
@@ -112,7 +112,7 @@ class ClientFrameViewLinux : public FramelessView,
|
||||
gfx::Insets GetTitlebarContentInsets() const;
|
||||
gfx::Rect GetTitlebarContentBounds() const;
|
||||
|
||||
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
|
||||
std::unique_ptr<LinuxCSDNativeFrameLayout> linux_frame_layout_;
|
||||
|
||||
raw_ptr<ui::NativeTheme> theme_;
|
||||
ThemeValues theme_values_;
|
||||
|
||||
@@ -4,14 +4,20 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/ui/views/linux_frame_layout.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
|
||||
#include "shell/browser/linux/x11_util.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/gfx/geometry/insets.h"
|
||||
#include "ui/gfx/geometry/skia_conversions.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/native_theme/native_theme.h"
|
||||
#include "ui/linux/window_frame_provider.h"
|
||||
#include "ui/views/layout/layout_provider.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
namespace electron {
|
||||
@@ -21,151 +27,174 @@ namespace {
|
||||
constexpr int kResizeBorder = 10;
|
||||
// This should match FramelessView's inside resize band.
|
||||
constexpr int kResizeInsideBoundsSize = 5;
|
||||
// These should match Chromium's restored frame edge thickness.
|
||||
constexpr gfx::Insets kDefaultCustomFrameBorder = gfx::Insets::TLBR(2, 1, 1, 1);
|
||||
|
||||
bool CheckClientFrameShadowSupport(NativeWindowViews* window) {
|
||||
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
|
||||
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
|
||||
window->GetAcceleratedWidget()));
|
||||
return tree_host && tree_host->SupportsClientFrameShadow();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
std::unique_ptr<LinuxFrameLayout> LinuxFrameLayout::Create(
|
||||
NativeWindowViews* window,
|
||||
bool wants_shadow) {
|
||||
bool wants_shadow,
|
||||
CSDStyle csd_style) {
|
||||
if (x11_util::IsX11() || window->IsTranslucent() || !wants_shadow) {
|
||||
return std::make_unique<LinuxUndecoratedFrameLayout>(window);
|
||||
return std::make_unique<LinuxFrameLayout>(window);
|
||||
} else if (csd_style == CSDStyle::kCustom) {
|
||||
return std::make_unique<LinuxCSDCustomFrameLayout>(window);
|
||||
} else {
|
||||
return std::make_unique<LinuxCSDFrameLayout>(window);
|
||||
return std::make_unique<LinuxCSDNativeFrameLayout>(window);
|
||||
}
|
||||
}
|
||||
|
||||
LinuxCSDFrameLayout::LinuxCSDFrameLayout(NativeWindowViews* window)
|
||||
: window_(window) {
|
||||
host_supports_client_frame_shadow_ = SupportsClientFrameShadow();
|
||||
gfx::Insets LinuxFrameLayout::GetResizeBorderInsets() const {
|
||||
gfx::Insets insets = RestoredFrameBorderInsets();
|
||||
return insets.IsEmpty() ? GetInputInsets() : insets;
|
||||
}
|
||||
|
||||
bool LinuxCSDFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxCSDFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDFrameLayout::RestoredFrameBorderInsets() const {
|
||||
gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
|
||||
const gfx::Insets input = GetInputInsets();
|
||||
|
||||
auto expand_if_visible = [](int side_thickness, int min_band) {
|
||||
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
|
||||
};
|
||||
|
||||
gfx::Insets merged;
|
||||
merged.set_top(expand_if_visible(insets.top(), input.top()));
|
||||
merged.set_left(expand_if_visible(insets.left(), input.left()));
|
||||
merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom()));
|
||||
merged.set_right(expand_if_visible(insets.right(), input.right()));
|
||||
|
||||
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
|
||||
merged.bottom(), merged.left())
|
||||
: merged;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDFrameLayout::GetInputInsets() const {
|
||||
bool showing_shadow = host_supports_client_frame_shadow_ &&
|
||||
!window_->IsMaximized() && !window_->IsFullscreen();
|
||||
return gfx::Insets(showing_shadow ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
bool LinuxCSDFrameLayout::SupportsClientFrameShadow() const {
|
||||
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
|
||||
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
|
||||
window_->GetAcceleratedWidget()));
|
||||
return tree_host->SupportsClientFrameShadow();
|
||||
}
|
||||
|
||||
void LinuxCSDFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) {
|
||||
GetFrameProvider()->PaintWindowFrame(
|
||||
canvas, local_bounds, titlebar_bounds.bottom(), active, GetInputInsets());
|
||||
}
|
||||
|
||||
gfx::Rect LinuxCSDFrameLayout::GetWindowContentBounds() const {
|
||||
gfx::Rect content_bounds = window_->widget()->GetWindowBoundsInScreen();
|
||||
content_bounds.Inset(RestoredFrameBorderInsets());
|
||||
return content_bounds;
|
||||
}
|
||||
|
||||
SkRRect LinuxCSDFrameLayout::GetRoundedWindowContentBounds() const {
|
||||
SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
|
||||
SkRRect LinuxFrameLayout::GetRoundedWindowBounds() const {
|
||||
SkRect rect = gfx::RectToSkRect(GetWindowBounds());
|
||||
SkRRect rrect;
|
||||
|
||||
if (!window_->IsMaximized()) {
|
||||
float radius = GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
float radius = GetTopCornerRadiusDip();
|
||||
if (radius > 0) {
|
||||
SkPoint round_point{radius, radius};
|
||||
SkPoint radii[] = {round_point, round_point, {}, {}};
|
||||
rrect.setRectRadii(rect, radii);
|
||||
} else {
|
||||
rrect.setRect(rect);
|
||||
}
|
||||
|
||||
return rrect;
|
||||
}
|
||||
|
||||
int LinuxCSDFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
// Base implementation is suitable for X11/views without shadows
|
||||
LinuxFrameLayout::LinuxFrameLayout(NativeWindowViews* window)
|
||||
: window_(window) {
|
||||
host_supports_client_frame_shadow_ = false;
|
||||
}
|
||||
|
||||
LinuxFrameLayout::~LinuxFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
|
||||
return gfx::Insets();
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(kResizeInsideBoundsSize);
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::IsShowingShadow() const {
|
||||
return host_supports_client_frame_shadow_ && !window_->IsMaximized() &&
|
||||
!window_->IsFullscreen();
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::SupportsClientFrameShadow() const {
|
||||
return host_supports_client_frame_shadow_;
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
|
||||
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
|
||||
bounds.Inset(RestoredFrameBorderInsets());
|
||||
return bounds;
|
||||
}
|
||||
|
||||
float LinuxFrameLayout::GetTopCornerRadiusDip() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxCSDFrameLayout::GetFrameProvider() const {
|
||||
int LinuxFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::NormalizeBorderInsets(
|
||||
const gfx::Insets& frame_insets,
|
||||
const gfx::Insets& input_insets) const {
|
||||
auto expand_if_visible = [](int side_thickness, int min_band) {
|
||||
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
|
||||
};
|
||||
|
||||
// Ensure hit testing for resize targets works
|
||||
// even if borders/shadows are absent on some edges.
|
||||
gfx::Insets merged;
|
||||
merged.set_top(expand_if_visible(frame_insets.top(), input_insets.top()));
|
||||
merged.set_left(expand_if_visible(frame_insets.left(), input_insets.left()));
|
||||
merged.set_bottom(
|
||||
expand_if_visible(frame_insets.bottom(), input_insets.bottom()));
|
||||
merged.set_right(
|
||||
expand_if_visible(frame_insets.right(), input_insets.right()));
|
||||
|
||||
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
|
||||
merged.bottom(), merged.left())
|
||||
: merged;
|
||||
}
|
||||
|
||||
// Used for a native-like frame with a FrameProvider
|
||||
LinuxCSDNativeFrameLayout::LinuxCSDNativeFrameLayout(NativeWindowViews* window)
|
||||
: LinuxFrameLayout(window) {
|
||||
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
|
||||
}
|
||||
|
||||
LinuxCSDNativeFrameLayout::~LinuxCSDNativeFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxCSDNativeFrameLayout::RestoredFrameBorderInsets() const {
|
||||
const gfx::Insets input_insets = GetInputInsets();
|
||||
const gfx::Insets frame_insets = GetFrameProvider()->GetFrameThicknessDip();
|
||||
return NormalizeBorderInsets(frame_insets, input_insets);
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDNativeFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
float LinuxCSDNativeFrameLayout::GetTopCornerRadiusDip() const {
|
||||
return window_->IsMaximized() ? 0
|
||||
: GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxCSDNativeFrameLayout::GetFrameProvider() const {
|
||||
return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
|
||||
!host_supports_client_frame_shadow_, tiled(), window_->IsMaximized());
|
||||
}
|
||||
|
||||
LinuxUndecoratedFrameLayout::LinuxUndecoratedFrameLayout(
|
||||
NativeWindowViews* window)
|
||||
: window_(window) {}
|
||||
|
||||
gfx::Insets LinuxUndecoratedFrameLayout::RestoredFrameBorderInsets() const {
|
||||
return gfx::Insets();
|
||||
// Used for Chromium-like custom CSD
|
||||
LinuxCSDCustomFrameLayout::LinuxCSDCustomFrameLayout(NativeWindowViews* window)
|
||||
: LinuxFrameLayout(window) {
|
||||
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
|
||||
}
|
||||
|
||||
gfx::Insets LinuxUndecoratedFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(kResizeInsideBoundsSize);
|
||||
LinuxCSDCustomFrameLayout::~LinuxCSDCustomFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxCSDCustomFrameLayout::RestoredFrameBorderInsets() const {
|
||||
const gfx::Insets input_insets = GetInputInsets();
|
||||
const bool showing_shadow = IsShowingShadow();
|
||||
const auto shadow_values = (showing_shadow && !tiled())
|
||||
? GetFrameShadowValuesLinux(/*active=*/true)
|
||||
: gfx::ShadowValues();
|
||||
const gfx::Insets frame_insets = GetRestoredFrameBorderInsetsLinux(
|
||||
showing_shadow, kDefaultCustomFrameBorder, shadow_values, input_insets);
|
||||
return NormalizeBorderInsets(frame_insets, input_insets);
|
||||
}
|
||||
|
||||
bool LinuxUndecoratedFrameLayout::SupportsClientFrameShadow() const {
|
||||
return false;
|
||||
gfx::Insets LinuxCSDCustomFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
bool LinuxUndecoratedFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxUndecoratedFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
void LinuxUndecoratedFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
gfx::Rect LinuxUndecoratedFrameLayout::GetWindowContentBounds() const {
|
||||
// With no transparent insets, widget bounds and logical bounds match.
|
||||
return window_->widget()->GetWindowBoundsInScreen();
|
||||
}
|
||||
|
||||
SkRRect LinuxUndecoratedFrameLayout::GetRoundedWindowContentBounds() const {
|
||||
SkRRect rrect;
|
||||
rrect.setRect(gfx::RectToSkRect(GetWindowContentBounds()));
|
||||
return rrect;
|
||||
}
|
||||
|
||||
int LinuxUndecoratedFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxUndecoratedFrameLayout::GetFrameProvider() const {
|
||||
return nullptr;
|
||||
gfx::ShadowValues GetFrameShadowValuesLinux(bool active) {
|
||||
const int elevation = views::LayoutProvider::Get()->GetShadowElevationMetric(
|
||||
active ? views::Emphasis::kMaximum : views::Emphasis::kMedium);
|
||||
return gfx::ShadowValue::MakeMdShadowValues(elevation);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -8,110 +8,96 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "shell/browser/linux/x11_util.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/base/ozone_buildflags.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "ui/gfx/geometry/insets.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/gfx/shadow_value.h"
|
||||
#include "ui/linux/window_frame_provider.h"
|
||||
|
||||
namespace gfx {
|
||||
class Insets;
|
||||
class Rect;
|
||||
} // namespace gfx
|
||||
|
||||
namespace electron {
|
||||
|
||||
class NativeWindowViews;
|
||||
|
||||
// Shared helper for CSD layout and frame painting on Linux (shadows, resize
|
||||
// regions, titlebars, etc.). Also helps views determine insets and perform
|
||||
// bounds conversions between widget and logical coordinates.
|
||||
// Shared helper for CSD layout on Linux (shadows, resize regions, titlebars,
|
||||
// etc.). Also helps views determine insets and perform bounds conversions
|
||||
// between widget and logical coordinates.
|
||||
//
|
||||
// The base class is concrete and suitable as-is for the undecorated case (X11,
|
||||
// translucent windows, or windows without shadows). CSD subclasses override
|
||||
// the methods that differ.
|
||||
class LinuxFrameLayout {
|
||||
public:
|
||||
virtual ~LinuxFrameLayout() = default;
|
||||
enum class CSDStyle {
|
||||
kNativeFrame,
|
||||
kCustom,
|
||||
};
|
||||
|
||||
explicit LinuxFrameLayout(NativeWindowViews* window);
|
||||
virtual ~LinuxFrameLayout();
|
||||
|
||||
static std::unique_ptr<LinuxFrameLayout> Create(NativeWindowViews* window,
|
||||
bool wants_shadow);
|
||||
bool wants_shadow,
|
||||
CSDStyle csd_style);
|
||||
|
||||
// Insets from the transparent widget border to the opaque part of the window
|
||||
virtual gfx::Insets RestoredFrameBorderInsets() const = 0;
|
||||
// Insets for parts of the surface that should be counted for user input
|
||||
virtual gfx::Insets GetInputInsets() const = 0;
|
||||
// Insets from the transparent widget border to the opaque part of the window.
|
||||
virtual gfx::Insets RestoredFrameBorderInsets() const;
|
||||
// Insets for parts of the surface that should be counted for user input.
|
||||
virtual gfx::Insets GetInputInsets() const;
|
||||
// Insets to use for non-client resize hit-testing.
|
||||
gfx::Insets GetResizeBorderInsets() const;
|
||||
|
||||
virtual bool SupportsClientFrameShadow() const = 0;
|
||||
bool IsShowingShadow() const;
|
||||
bool SupportsClientFrameShadow() const;
|
||||
|
||||
virtual bool tiled() const = 0;
|
||||
virtual void set_tiled(bool tiled) = 0;
|
||||
bool tiled() const;
|
||||
void set_tiled(bool tiled);
|
||||
|
||||
virtual void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) = 0;
|
||||
// The logical bounds of the window interior.
|
||||
gfx::Rect GetWindowBounds() const;
|
||||
// The logical window bounds as a rounded rect with corner radii applied.
|
||||
SkRRect GetRoundedWindowBounds() const;
|
||||
// The corner radius of the top corners of the window, in DIPs.
|
||||
virtual float GetTopCornerRadiusDip() const;
|
||||
|
||||
// The logical bounds of the window
|
||||
virtual gfx::Rect GetWindowContentBounds() const = 0;
|
||||
// The logical bounds as a rounded rect with corner radii applied
|
||||
virtual SkRRect GetRoundedWindowContentBounds() const = 0;
|
||||
int GetTranslucentTopAreaHeight() const;
|
||||
|
||||
virtual int GetTranslucentTopAreaHeight() const = 0;
|
||||
protected:
|
||||
gfx::Insets NormalizeBorderInsets(const gfx::Insets& frame_insets,
|
||||
const gfx::Insets& input_insets) const;
|
||||
|
||||
virtual ui::WindowFrameProvider* GetFrameProvider() const = 0;
|
||||
};
|
||||
|
||||
// Client-side decoration (CSD) Linux frame layout implementation.
|
||||
class LinuxCSDFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxCSDFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDFrameLayout() override = default;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
bool SupportsClientFrameShadow() const override;
|
||||
bool tiled() const override;
|
||||
void set_tiled(bool tiled) override;
|
||||
void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) override;
|
||||
gfx::Rect GetWindowContentBounds() const override;
|
||||
SkRRect GetRoundedWindowContentBounds() const override;
|
||||
int GetTranslucentTopAreaHeight() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const override;
|
||||
|
||||
private:
|
||||
raw_ptr<NativeWindowViews> window_;
|
||||
bool tiled_ = false;
|
||||
bool host_supports_client_frame_shadow_ = false;
|
||||
};
|
||||
|
||||
// No-decoration Linux frame layout implementation.
|
||||
//
|
||||
// Intended for cases where we do not allocate a transparent inset area around
|
||||
// the window (e.g. X11 / server-side decorations, or when insets are disabled).
|
||||
// All inset math returns 0 and frame painting is skipped.
|
||||
class LinuxUndecoratedFrameLayout : public LinuxFrameLayout {
|
||||
// CSD strategy that uses the GTK window frame provider for metrics.
|
||||
class LinuxCSDNativeFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxUndecoratedFrameLayout(NativeWindowViews* window);
|
||||
~LinuxUndecoratedFrameLayout() override = default;
|
||||
explicit LinuxCSDNativeFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDNativeFrameLayout() override;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
bool SupportsClientFrameShadow() const override;
|
||||
bool tiled() const override;
|
||||
void set_tiled(bool tiled) override;
|
||||
void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) override;
|
||||
gfx::Rect GetWindowContentBounds() const override;
|
||||
SkRRect GetRoundedWindowContentBounds() const override;
|
||||
int GetTranslucentTopAreaHeight() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const override;
|
||||
|
||||
private:
|
||||
raw_ptr<NativeWindowViews> window_;
|
||||
bool tiled_ = false;
|
||||
float GetTopCornerRadiusDip() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const;
|
||||
};
|
||||
|
||||
// CSD strategy that uses custom metrics, similar to those used in Chromium.
|
||||
class LinuxCSDCustomFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxCSDCustomFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDCustomFrameLayout() override;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
};
|
||||
|
||||
gfx::ShadowValues GetFrameShadowValuesLinux(bool active);
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_LINUX_FRAME_LAYOUT_H_
|
||||
|
||||
@@ -5,22 +5,24 @@
|
||||
#include "shell/browser/ui/views/opaque_frame_view.h"
|
||||
|
||||
#include "base/containers/adapters.h"
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
|
||||
#include "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h" // nogncheck
|
||||
#include "chrome/grit/generated_resources.h"
|
||||
#include "components/strings/grit/components_strings.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/base/hit_test.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/metadata/metadata_impl_macros.h"
|
||||
#include "ui/compositor/layer.h"
|
||||
#include "ui/gfx/font_list.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/gfx/geometry/insets_f.h"
|
||||
#include "ui/gfx/geometry/skia_conversions.h"
|
||||
#include "ui/views/accessibility/view_accessibility.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
#include "ui/views/widget/widget_delegate.h"
|
||||
#include "ui/views/window/frame_background.h"
|
||||
#include "ui/views/window/frame_caption_button.h"
|
||||
#include "ui/views/window/vector_icons/vector_icons.h"
|
||||
|
||||
@@ -55,12 +57,14 @@ const int kCaptionButtonBottomPadding = 3;
|
||||
// The content edge images have a shadow built into them.
|
||||
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
|
||||
|
||||
OpaqueFrameView::OpaqueFrameView() = default;
|
||||
OpaqueFrameView::OpaqueFrameView()
|
||||
: frame_background_(std::make_unique<views::FrameBackground>()) {}
|
||||
OpaqueFrameView::~OpaqueFrameView() = default;
|
||||
|
||||
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
|
||||
FramelessView::Init(window, frame);
|
||||
linux_frame_layout_ = LinuxFrameLayout::Create(window, window->HasShadow());
|
||||
linux_frame_layout_ = LinuxFrameLayout::Create(
|
||||
window, window->HasShadow(), LinuxFrameLayout::CSDStyle::kCustom);
|
||||
|
||||
// Unretained() is safe because the subscription is saved into an instance
|
||||
// member and thus will be cancelled upon the instance's destruction.
|
||||
@@ -98,9 +102,8 @@ void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
|
||||
}
|
||||
|
||||
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
|
||||
auto insets = RestoredFrameBorderInsets();
|
||||
return ResizingBorderHitTestImpl(
|
||||
point, insets.IsEmpty() ? linux_frame_layout_->GetInputInsets() : insets);
|
||||
point, linux_frame_layout_->GetResizeBorderInsets());
|
||||
}
|
||||
|
||||
void OpaqueFrameView::InvalidateCaptionButtons() {
|
||||
@@ -200,14 +203,31 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
|
||||
if (frame()->IsFullscreen())
|
||||
return;
|
||||
|
||||
// Titlebar height must be at least the frame border insets to avoid
|
||||
// a negative height calculation in the GTK frame provider. We add 1 to
|
||||
// ensure it's always positive even when insets are 0.
|
||||
int top_area_height = RestoredFrameBorderInsets().top() + 1;
|
||||
const bool active = ShouldPaintAsActive();
|
||||
const gfx::Insets border = RestoredFrameBorderInsets();
|
||||
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
|
||||
gfx::RectF bounds_dip(GetLocalBounds());
|
||||
if (showing_shadow) {
|
||||
bounds_dip.Inset(gfx::InsetsF(border));
|
||||
}
|
||||
|
||||
linux_frame_layout_->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), gfx::Rect(0, 0, width(), top_area_height),
|
||||
ShouldPaintAsActive());
|
||||
// TODO: support roundedCorners.
|
||||
float radius_dip = 0;
|
||||
SkVector radii[4]{{radius_dip, radius_dip}, {radius_dip, radius_dip}, {}, {}};
|
||||
SkRRect clip;
|
||||
clip.setRectRadii(gfx::RectFToSkRect(bounds_dip), radii);
|
||||
|
||||
frame_background_->set_frame_color(GetFrameColor());
|
||||
frame_background_->set_use_custom_frame(true);
|
||||
frame_background_->set_is_active(active);
|
||||
frame_background_->set_top_area_height(GetTopAreaHeight());
|
||||
|
||||
const bool draw_shadow = showing_shadow && !linux_frame_layout_->tiled();
|
||||
auto shadow_values =
|
||||
draw_shadow ? GetFrameShadowValuesLinux(active) : gfx::ShadowValues();
|
||||
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
|
||||
showing_shadow, active, border, shadow_values,
|
||||
linux_frame_layout_->tiled());
|
||||
|
||||
if (!window()->IsWindowControlsOverlayEnabled())
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
class CaptionButtonPlaceholderContainer;
|
||||
|
||||
namespace views {
|
||||
class FrameBackground;
|
||||
}
|
||||
|
||||
namespace electron {
|
||||
|
||||
class NativeWindowViews;
|
||||
@@ -166,6 +170,7 @@ class OpaqueFrameView : public FramelessView {
|
||||
bool is_leading_button) const;
|
||||
|
||||
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
|
||||
std::unique_ptr<views::FrameBackground> frame_background_;
|
||||
|
||||
// Window controls.
|
||||
raw_ptr<views::Button> minimize_button_;
|
||||
|
||||
@@ -266,7 +266,11 @@ gfx::Image Clipboard::ReadImage(gin::Arguments* const args) {
|
||||
[](std::optional<gfx::Image>* image, base::RepeatingClosure cb,
|
||||
const std::vector<uint8_t>& result) {
|
||||
SkBitmap bitmap = gfx::PNGCodec::Decode(result);
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
if (bitmap.isNull()) {
|
||||
image->emplace();
|
||||
} else {
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
}
|
||||
std::move(cb).Run();
|
||||
},
|
||||
&image, std::move(callback)));
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FILE_PATH_CONVERTER_H_
|
||||
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FILE_PATH_CONVERTER_H_
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "base/files/file_path.h"
|
||||
#include "gin/converter.h"
|
||||
#include "shell/common/gin_converters/std_converter.h"
|
||||
@@ -30,6 +32,11 @@ struct Converter<base::FilePath> {
|
||||
|
||||
base::FilePath::StringType path;
|
||||
if (Converter<base::FilePath::StringType>::FromV8(isolate, val, &path)) {
|
||||
bool has_control_chars = std::any_of(
|
||||
path.begin(), path.end(),
|
||||
[](base::FilePath::CharType c) { return c >= 0 && c < 0x20; });
|
||||
if (has_control_chars)
|
||||
return false;
|
||||
*out = base::FilePath(path);
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -607,6 +607,9 @@ bool Converter<scoped_refptr<network::ResourceRequestBody>>::FromV8(
|
||||
const std::string* file = dict.FindString("filePath");
|
||||
if (!file)
|
||||
return false;
|
||||
if (std::any_of(file->begin(), file->end(),
|
||||
[](char c) { return c >= 0 && c < 0x20; }))
|
||||
return false;
|
||||
double modification_time =
|
||||
dict.FindDouble("modificationTime").value_or(0.0);
|
||||
int offset = dict.FindInt("offset").value_or(0);
|
||||
|
||||
@@ -153,9 +153,12 @@ v8::Local<v8::Value> Converter<electron::OffscreenSharedTextureValue>::ToV8(
|
||||
root.Set("textureInfo", ConvertToV8(isolate, dict));
|
||||
auto root_local = ConvertToV8(isolate, root);
|
||||
|
||||
// Create a persistent reference of the object, so that we can check the
|
||||
// monitor again when GC collects this object.
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, root_local);
|
||||
// Create a weak persistent that tracks the release function rather than the
|
||||
// texture object. The release function holds a raw pointer to |monitor| via
|
||||
// its v8::External data, so |monitor| must outlive it. Since the texture
|
||||
// keeps |release| alive via its property, this also covers the case where
|
||||
// the texture itself is leaked without calling release().
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, releaser);
|
||||
tex_persistent->SetWeak(
|
||||
monitor,
|
||||
[](const v8::WeakCallbackInfo<OffscreenReleaseHolderMonitor>& data) {
|
||||
|
||||
@@ -6902,6 +6902,54 @@ describe('BrowserWindow module', () => {
|
||||
expect(w.webContents.frameRate).to.equal(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared texture', () => {
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
|
||||
it('does not crash when release() is called after the texture is garbage collected', async () => {
|
||||
const sw = new BrowserWindow({
|
||||
width: 100,
|
||||
height: 100,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
offscreen: {
|
||||
useSharedTexture: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const paint = once(sw.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
sw.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [event] = await paint;
|
||||
sw.webContents.stopPainting();
|
||||
|
||||
if (!event.texture) {
|
||||
// GPU shared texture not available on this host; skip.
|
||||
sw.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep only the release closure and drop the owning texture object.
|
||||
const staleRelease = event.texture.release;
|
||||
const weakTexture = new WeakRef(event.texture);
|
||||
event.texture = undefined;
|
||||
|
||||
// Force GC until the texture object is collected.
|
||||
let collected = false;
|
||||
for (let i = 0; i < 30 && !collected; ++i) {
|
||||
await setTimeout();
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
collected = weakTexture.deref() === undefined;
|
||||
}
|
||||
expect(collected).to.be.true('texture should be garbage collected');
|
||||
|
||||
// This should return safely and not crash the main process.
|
||||
expect(() => staleRelease()).to.not.throw();
|
||||
|
||||
sw.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"transparent" option', () => {
|
||||
|
||||
@@ -132,6 +132,36 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTraceBufferUsage', function () {
|
||||
this.timeout(10e3);
|
||||
|
||||
it('does not crash and returns valid usage data', async () => {
|
||||
await app.whenReady();
|
||||
await contentTracing.startRecording({
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full'
|
||||
});
|
||||
|
||||
// Yield to the event loop so the JS HandleScope from this tick is gone.
|
||||
// When the Mojo response arrives it fires OnTraceBufferUsageAvailable
|
||||
// as a plain Chromium task — if that callback lacks its own HandleScope
|
||||
// the process will crash with "Cannot create a handle without a HandleScope".
|
||||
const result = await contentTracing.getTraceBufferUsage();
|
||||
|
||||
expect(result).to.have.property('percentage').that.is.a('number');
|
||||
expect(result).to.have.property('value').that.is.a('number');
|
||||
|
||||
await contentTracing.stopRecording();
|
||||
});
|
||||
|
||||
it('returns zero usage when no trace is active', async () => {
|
||||
await app.whenReady();
|
||||
const result = await contentTracing.getTraceBufferUsage();
|
||||
expect(result).to.have.property('percentage').that.is.a('number');
|
||||
expect(result.percentage).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('captured events', () => {
|
||||
it('include V8 samples from the main process', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
@@ -2,9 +2,10 @@ import { dialog, BaseWindow, BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifit } from './lib/spec-helpers';
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('dialog module', () => {
|
||||
@@ -243,4 +244,785 @@ describe('dialog module', () => {
|
||||
}).to.throw(/message must be a string/);
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin' && !process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS)('end-to-end dialog interaction (macOS)', () => {
|
||||
let dialogHelper: any;
|
||||
|
||||
before(() => {
|
||||
dialogHelper = require('@electron-ci/dialog-helper');
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
// Poll for a sheet to appear on the given window.
|
||||
async function waitForSheet (w: BrowserWindow): Promise<void> {
|
||||
const handle = w.getNativeWindowHandle();
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
if (info.type !== 'none') return;
|
||||
await setTimeout(100);
|
||||
}
|
||||
throw new Error('Timed out waiting for dialog sheet to appear');
|
||||
}
|
||||
|
||||
describe('showMessageBox', () => {
|
||||
it('shows the correct message and buttons', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Test message',
|
||||
buttons: ['OK', 'Cancel']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
expect(info.message).to.equal('Test message');
|
||||
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.include('OK');
|
||||
expect(buttons).to.include('Cancel');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows detail text', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Main message',
|
||||
detail: 'Extra detail text',
|
||||
buttons: ['OK']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.message).to.equal('Main message');
|
||||
expect(info.detail).to.equal('Extra detail text');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('returns the correct response when a specific button is clicked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Choose a button',
|
||||
buttons: ['First', 'Second', 'Third']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
dialogHelper.clickMessageBoxButton(handle, 1);
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(1);
|
||||
});
|
||||
|
||||
it('returns the correct response when the last button is clicked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Choose a button',
|
||||
buttons: ['Yes', 'No', 'Maybe']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
dialogHelper.clickMessageBoxButton(handle, 2);
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('shows a single button when no buttons are specified', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'No buttons specified'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
// macOS adds a default "OK" button when none are specified.
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.have.lengthOf(1);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(0);
|
||||
});
|
||||
|
||||
it('renders checkbox with the correct label and initial state', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Checkbox test',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Do not show again',
|
||||
checkboxChecked: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.checkboxLabel).to.equal('Do not show again');
|
||||
expect(info.checkboxChecked).to.be.false();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.false();
|
||||
});
|
||||
|
||||
it('returns checkboxChecked as true when checkbox is initially checked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Pre-checked checkbox',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Remember my choice',
|
||||
checkboxChecked: true
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.checkboxLabel).to.equal('Remember my choice');
|
||||
expect(info.checkboxChecked).to.be.true();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.true();
|
||||
});
|
||||
|
||||
it('can toggle checkbox and returns updated state', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Toggle test',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Toggle me',
|
||||
checkboxChecked: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
// Verify initially unchecked.
|
||||
let info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.checkboxChecked).to.be.false();
|
||||
|
||||
// Click the checkbox to check it.
|
||||
dialogHelper.clickCheckbox(handle);
|
||||
info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.checkboxChecked).to.be.true();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.true();
|
||||
});
|
||||
|
||||
it('strips access keys on macOS with normalizeAccessKeys', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Access key test',
|
||||
buttons: ['&Save', '&Cancel'],
|
||||
normalizeAccessKeys: true
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
// On macOS, ampersands are stripped by normalizeAccessKeys.
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.include('Save');
|
||||
expect(buttons).to.include('Cancel');
|
||||
expect(buttons).not.to.include('&Save');
|
||||
expect(buttons).not.to.include('&Cancel');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('respects defaultId by making it the default button', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Default button test',
|
||||
buttons: ['One', 'Two', 'Three'],
|
||||
defaultId: 2
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.deep.equal(['One', 'Two', 'Three']);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 2);
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('respects cancelId and returns it when cancelled via signal', async () => {
|
||||
const controller = new AbortController();
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Cancel ID test',
|
||||
buttons: ['OK', 'Dismiss', 'Abort'],
|
||||
cancelId: 2,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
controller.abort();
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('works with all message box types', async () => {
|
||||
const types: Array<'none' | 'info' | 'warning' | 'error' | 'question'> =
|
||||
['none', 'info', 'warning', 'error', 'question'];
|
||||
|
||||
for (const type of types) {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: `Type: ${type}`,
|
||||
type,
|
||||
buttons: ['OK']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
expect(info.message).to.equal(`Type: ${type}`);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
w.destroy();
|
||||
// Allow the event loop to settle between iterations to avoid
|
||||
// Chromium DCHECK failures from rapid window lifecycle churn.
|
||||
await setTimeout(100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('showOpenDialog', () => {
|
||||
it('can cancel an open dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
title: 'Test Open',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.type).to.equal('open-dialog');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.true();
|
||||
expect(result.filePaths).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('sets a custom button label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
buttonLabel: 'Select This',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.prompt).to.equal('Select This');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a message on the dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
message: 'Choose a file to import',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.panelMessage).to.equal('Choose a file to import');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('defaults to openFile with canChooseFiles enabled', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.false();
|
||||
expect(info.allowsMultipleSelection).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory selection with openDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
// openFile is not set, so canChooseFiles should be false
|
||||
expect(info.canChooseFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables both file and directory selection together', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables multiple selection with multiSelections', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'multiSelections']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.allowsMultipleSelection).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows hidden files with showHiddenFiles', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'showHiddenFiles']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('does not show hidden files by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('disables alias resolution with noResolveAliases', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'noResolveAliases']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.resolvesAliases).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('resolves aliases by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.resolvesAliases).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('treats packages as directories with treatPackageAsDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'treatPackageAsDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory creation with createDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default path directory', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
defaultPath: defaultDir,
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('applies multiple properties simultaneously', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
title: 'Multi-Property Test',
|
||||
buttonLabel: 'Pick',
|
||||
message: 'Select items',
|
||||
properties: [
|
||||
'openFile',
|
||||
'openDirectory',
|
||||
'multiSelections',
|
||||
'showHiddenFiles',
|
||||
'createDirectory',
|
||||
'treatPackageAsDirectory',
|
||||
'noResolveAliases'
|
||||
]
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('open-dialog');
|
||||
expect(info.prompt).to.equal('Pick');
|
||||
expect(info.panelMessage).to.equal('Select items');
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
expect(info.allowsMultipleSelection).to.be.true();
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
expect(info.resolvesAliases).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('can accept an open dialog and return a file path', async () => {
|
||||
const targetDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
defaultPath: targetDir,
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
dialogHelper.acceptFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.false();
|
||||
expect(result.filePaths).to.have.lengthOf(1);
|
||||
expect(result.filePaths[0]).to.equal(targetDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showSaveDialog', () => {
|
||||
it('can cancel a save dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
title: 'Test Save'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.type).to.equal('save-dialog');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.true();
|
||||
expect(result.filePath).to.equal('');
|
||||
});
|
||||
|
||||
it('can accept a save dialog with a filename', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const filename = 'test-save-output.txt';
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
title: 'Test Save',
|
||||
defaultPath: path.join(defaultDir, filename)
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
dialogHelper.acceptFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.false();
|
||||
expect(result.filePath).to.equal(path.join(defaultDir, filename));
|
||||
});
|
||||
|
||||
it('sets a custom button label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
buttonLabel: 'Export'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.prompt).to.equal('Export');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a message on the dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
message: 'Choose where to save'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.panelMessage).to.equal('Choose where to save');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a custom name field label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
nameFieldLabel: 'Export As:'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.nameFieldLabel).to.equal('Export As:');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default filename from defaultPath', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
defaultPath: path.join(__dirname, 'fixtures', 'my-document.txt')
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.nameFieldValue).to.equal('my-document.txt');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default directory from defaultPath', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
defaultPath: path.join(defaultDir, 'some-file.txt')
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('hides the tag field when showsTagField is false', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
showsTagField: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsTagField).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows the tag field by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsTagField).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory creation with createDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows hidden files with showHiddenFiles', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['showHiddenFiles']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('does not show hidden files by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('treats packages as directories with treatPackageAsDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['treatPackageAsDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('applies multiple options simultaneously', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
buttonLabel: 'Save Now',
|
||||
message: 'Pick a location',
|
||||
nameFieldLabel: 'File Name:',
|
||||
defaultPath: path.join(defaultDir, 'output.txt'),
|
||||
showsTagField: false,
|
||||
properties: ['showHiddenFiles', 'createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('save-dialog');
|
||||
expect(info.prompt).to.equal('Save Now');
|
||||
expect(info.panelMessage).to.equal('Pick a location');
|
||||
expect(info.nameFieldLabel).to.equal('File Name:');
|
||||
expect(info.nameFieldValue).to.equal('output.txt');
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
expect(info.showsTagField).to.be.false();
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('nativeTheme module', () => {
|
||||
@@ -119,4 +120,10 @@ describe('nativeTheme module', () => {
|
||||
expect(nativeTheme.prefersReducedTransparency).to.be.a('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('nativeTheme.shouldDifferentiateWithoutColor', () => {
|
||||
it('returns a boolean', () => {
|
||||
expect(nativeTheme.shouldDifferentiateWithoutColor).to.be.a('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,22 @@ describe('utilityProcess module', () => {
|
||||
expect(code).to.equal(exitCode);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'win32')('emits correct exit code when high bit is set on Windows', async () => {
|
||||
// NTSTATUS code with high bit set should not be mangled by sign extension.
|
||||
const exitCode = 0xC0000005;
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
|
||||
const [code] = await once(child, 'exit');
|
||||
expect(code).to.equal(exitCode);
|
||||
});
|
||||
|
||||
ifit(process.platform !== 'win32')('emits correct exit code when child process crashes on posix', async () => {
|
||||
// Crash exit codes should not be sign-extended to large 64-bit values.
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
|
||||
const [code] = await once(child, 'exit');
|
||||
expect(code).to.not.equal(0);
|
||||
expect(code).to.be.lessThanOrEqual(0xFFFFFFFF);
|
||||
});
|
||||
|
||||
it('does not run JS after process.exit is called', async () => {
|
||||
const file = path.join(os.tmpdir(), `no-js-after-exit-log-${Math.random()}`);
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'no-js-after-exit.js'), [`--testPath=${file}`]);
|
||||
|
||||
7
spec/fixtures/crash-cases/dialog-on-invalid-url/index.html
vendored
Normal file
7
spec/fixtures/crash-cases/dialog-on-invalid-url/index.html
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
window.open('javascript:alert()');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
spec/fixtures/crash-cases/dialog-on-invalid-url/index.js
vendored
Normal file
22
spec/fixtures/crash-cases/dialog-on-invalid-url/index.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error(reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
window.webContents.once('did-frame-navigate', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const win = new BrowserWindow({ show: false });
|
||||
win.loadFile('index.html');
|
||||
});
|
||||
7
spec/fixtures/native-addon/dialog-helper/.gitignore
vendored
Normal file
7
spec/fixtures/native-addon/dialog-helper/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/node_modules
|
||||
/build
|
||||
*.swp
|
||||
*.log
|
||||
*~
|
||||
.node-version
|
||||
package-lock.json
|
||||
23
spec/fixtures/native-addon/dialog-helper/binding.gyp
vendored
Normal file
23
spec/fixtures/native-addon/dialog-helper/binding.gyp
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'dialog_helper',
|
||||
'conditions': [
|
||||
['OS=="mac"', {
|
||||
'sources': [
|
||||
'src/main.cc',
|
||||
'src/dialog_helper_mac.mm',
|
||||
],
|
||||
'libraries': [
|
||||
'$(SDKROOT)/System/Library/Frameworks/AppKit.framework',
|
||||
],
|
||||
'xcode_settings': {
|
||||
'OTHER_CFLAGS': ['-fobjc-arc'],
|
||||
},
|
||||
}, {
|
||||
'type': 'none',
|
||||
}],
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
2
spec/fixtures/native-addon/dialog-helper/lib/index.js
vendored
Normal file
2
spec/fixtures/native-addon/dialog-helper/lib/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
const binding = require('../build/Release/dialog_helper.node');
|
||||
module.exports = binding;
|
||||
10
spec/fixtures/native-addon/dialog-helper/package.json
vendored
Normal file
10
spec/fixtures/native-addon/dialog-helper/package.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@electron-ci/dialog-helper",
|
||||
"version": "0.0.1",
|
||||
"main": "./lib/index.js",
|
||||
"private": true,
|
||||
"licenses": "MIT",
|
||||
"scripts": {
|
||||
"install": "node-gyp configure && node-gyp build"
|
||||
}
|
||||
}
|
||||
68
spec/fixtures/native-addon/dialog-helper/src/dialog_helper.h
vendored
Normal file
68
spec/fixtures/native-addon/dialog-helper/src/dialog_helper.h
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef SRC_DIALOG_HELPER_H_
|
||||
#define SRC_DIALOG_HELPER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace dialog_helper {
|
||||
|
||||
struct DialogInfo {
|
||||
// "message-box", "open-dialog", "save-dialog", or "none"
|
||||
std::string type;
|
||||
// Button titles for message boxes
|
||||
std::string buttons; // JSON array string, e.g. '["OK","Cancel"]'
|
||||
// Message text (NSAlert messageText or panel title)
|
||||
std::string message;
|
||||
// Detail / informative text (NSAlert informativeText)
|
||||
std::string detail;
|
||||
// Checkbox (suppression button) label, empty if none
|
||||
std::string checkbox_label;
|
||||
// Whether the checkbox is checked
|
||||
bool checkbox_checked = false;
|
||||
|
||||
// File dialog properties (open/save panels)
|
||||
std::string prompt; // Button label (NSSavePanel prompt)
|
||||
std::string panel_message; // Panel message text (NSSavePanel message)
|
||||
std::string directory; // Current directory URL path
|
||||
|
||||
// NSSavePanel-specific properties
|
||||
std::string name_field_label; // Label for the name field
|
||||
std::string name_field_value; // Current value of the name field
|
||||
bool shows_tag_field = true;
|
||||
|
||||
// NSOpenPanel-specific properties
|
||||
bool can_choose_files = false;
|
||||
bool can_choose_directories = false;
|
||||
bool allows_multiple_selection = false;
|
||||
|
||||
// Shared panel properties (open and save)
|
||||
bool shows_hidden_files = false;
|
||||
bool resolves_aliases = true;
|
||||
bool treats_packages_as_directories = false;
|
||||
bool can_create_directories = false;
|
||||
};
|
||||
|
||||
// Get information about the sheet dialog attached to the window identified
|
||||
// by the given native handle buffer (NSView* on macOS).
|
||||
DialogInfo GetDialogInfo(char* handle, size_t size);
|
||||
|
||||
// Click a button at the given index on an NSAlert sheet attached to the window.
|
||||
// Returns true if a message box was found and the button was clicked.
|
||||
bool ClickMessageBoxButton(char* handle, size_t size, int button_index);
|
||||
|
||||
// Toggle the checkbox (suppression button) on an NSAlert sheet.
|
||||
// Returns true if a checkbox was found and clicked.
|
||||
bool ClickCheckbox(char* handle, size_t size);
|
||||
|
||||
// Cancel the file dialog (NSOpenPanel/NSSavePanel) sheet attached to the window.
|
||||
// Returns true if a file dialog was found and cancelled.
|
||||
bool CancelFileDialog(char* handle, size_t size);
|
||||
|
||||
// Accept the file dialog sheet attached to the window.
|
||||
// For save dialogs, |filename| is set in the name field before accepting.
|
||||
// Returns true if a file dialog was found and accepted.
|
||||
bool AcceptFileDialog(char* handle, size_t size, const std::string& filename);
|
||||
|
||||
} // namespace dialog_helper
|
||||
|
||||
#endif // SRC_DIALOG_HELPER_H_
|
||||
320
spec/fixtures/native-addon/dialog-helper/src/dialog_helper_mac.mm
vendored
Normal file
320
spec/fixtures/native-addon/dialog-helper/src/dialog_helper_mac.mm
vendored
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "dialog_helper.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Extract the NSWindow* from the native handle buffer.
|
||||
// The buffer contains an NSView* (the content view of the window).
|
||||
NSWindow* GetNSWindowFromHandle(char* handle, size_t size) {
|
||||
if (size != sizeof(void*))
|
||||
return nil;
|
||||
// Read the raw pointer from the buffer, then bridge to ARC.
|
||||
void* raw = *reinterpret_cast<void**>(handle);
|
||||
NSView* view = (__bridge NSView*)raw;
|
||||
if (!view || ![view isKindOfClass:[NSView class]])
|
||||
return nil;
|
||||
return [view window];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace dialog_helper {
|
||||
|
||||
DialogInfo GetDialogInfo(char* handle, size_t size) {
|
||||
DialogInfo info;
|
||||
info.type = "none";
|
||||
|
||||
NSWindow* window = GetNSWindowFromHandle(handle, size);
|
||||
if (!window)
|
||||
return info;
|
||||
|
||||
NSWindow* sheet = [window attachedSheet];
|
||||
if (!sheet)
|
||||
return info;
|
||||
|
||||
// NSOpenPanel is a subclass of NSSavePanel, so check NSOpenPanel first.
|
||||
if ([sheet isKindOfClass:[NSOpenPanel class]]) {
|
||||
info.type = "open-dialog";
|
||||
NSOpenPanel* panel = (NSOpenPanel*)sheet;
|
||||
info.message = [[panel title] UTF8String] ?: "";
|
||||
info.prompt = [[panel prompt] UTF8String] ?: "";
|
||||
info.panel_message = [[panel message] UTF8String] ?: "";
|
||||
if ([panel directoryURL])
|
||||
info.directory = [[[panel directoryURL] path] UTF8String] ?: "";
|
||||
info.can_choose_files = [panel canChooseFiles];
|
||||
info.can_choose_directories = [panel canChooseDirectories];
|
||||
info.allows_multiple_selection = [panel allowsMultipleSelection];
|
||||
info.shows_hidden_files = [panel showsHiddenFiles];
|
||||
info.resolves_aliases = [panel resolvesAliases];
|
||||
info.treats_packages_as_directories = [panel treatsFilePackagesAsDirectories];
|
||||
info.can_create_directories = [panel canCreateDirectories];
|
||||
return info;
|
||||
}
|
||||
|
||||
if ([sheet isKindOfClass:[NSSavePanel class]]) {
|
||||
info.type = "save-dialog";
|
||||
NSSavePanel* panel = (NSSavePanel*)sheet;
|
||||
info.message = [[panel title] UTF8String] ?: "";
|
||||
info.prompt = [[panel prompt] UTF8String] ?: "";
|
||||
info.panel_message = [[panel message] UTF8String] ?: "";
|
||||
if ([panel directoryURL])
|
||||
info.directory = [[[panel directoryURL] path] UTF8String] ?: "";
|
||||
info.name_field_label = [[panel nameFieldLabel] UTF8String] ?: "";
|
||||
info.name_field_value = [[panel nameFieldStringValue] UTF8String] ?: "";
|
||||
info.shows_tag_field = [panel showsTagField];
|
||||
info.shows_hidden_files = [panel showsHiddenFiles];
|
||||
info.treats_packages_as_directories =
|
||||
[panel treatsFilePackagesAsDirectories];
|
||||
info.can_create_directories = [panel canCreateDirectories];
|
||||
return info;
|
||||
}
|
||||
|
||||
// For NSAlert, the sheet window is not an NSSavePanel.
|
||||
// Check if it contains typical NSAlert button structure.
|
||||
// NSAlert's window contains buttons as subviews in its content view.
|
||||
NSView* contentView = [sheet contentView];
|
||||
NSMutableArray<NSButton*>* buttons = [NSMutableArray array];
|
||||
|
||||
// Recursively find all NSButton instances in the view hierarchy.
|
||||
NSMutableArray<NSView*>* stack =
|
||||
[NSMutableArray arrayWithObject:contentView];
|
||||
while ([stack count] > 0) {
|
||||
NSView* current = [stack lastObject];
|
||||
[stack removeLastObject];
|
||||
if ([current isKindOfClass:[NSButton class]]) {
|
||||
NSButton* btn = (NSButton*)current;
|
||||
// Filter to push-type buttons (not checkboxes, radio buttons, etc.)
|
||||
if ([btn bezelStyle] == NSBezelStyleRounded ||
|
||||
[btn bezelStyle] == NSBezelStyleRegularSquare) {
|
||||
[buttons addObject:btn];
|
||||
}
|
||||
}
|
||||
for (NSView* subview in [current subviews]) {
|
||||
[stack addObject:subview];
|
||||
}
|
||||
}
|
||||
|
||||
if ([buttons count] > 0) {
|
||||
info.type = "message-box";
|
||||
|
||||
// Sort buttons by tag to maintain the order they were added.
|
||||
[buttons sortUsingComparator:^NSComparisonResult(NSButton* a, NSButton* b) {
|
||||
if ([a tag] < [b tag])
|
||||
return NSOrderedAscending;
|
||||
if ([a tag] > [b tag])
|
||||
return NSOrderedDescending;
|
||||
return NSOrderedSame;
|
||||
}];
|
||||
|
||||
std::string btn_json = "[";
|
||||
for (NSUInteger i = 0; i < [buttons count]; i++) {
|
||||
if (i > 0)
|
||||
btn_json += ",";
|
||||
btn_json += "\"";
|
||||
NSString* title = [[buttons objectAtIndex:i] title];
|
||||
btn_json += [title UTF8String] ?: "";
|
||||
btn_json += "\"";
|
||||
}
|
||||
btn_json += "]";
|
||||
info.buttons = btn_json;
|
||||
|
||||
// NSAlert's content view contains static NSTextFields for message and
|
||||
// detail text. The first non-editable text field with content is the
|
||||
// message; the second is the detail (informative text).
|
||||
int text_field_index = 0;
|
||||
// Walk all subviews (non-recursive — NSAlert places labels directly).
|
||||
for (NSView* subview in [contentView subviews]) {
|
||||
if ([subview isKindOfClass:[NSTextField class]]) {
|
||||
NSTextField* field = (NSTextField*)subview;
|
||||
if (![field isEditable] && [[field stringValue] length] > 0) {
|
||||
if (text_field_index == 0) {
|
||||
info.message = [[field stringValue] UTF8String];
|
||||
} else if (text_field_index == 1) {
|
||||
info.detail = [[field stringValue] UTF8String];
|
||||
}
|
||||
text_field_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for the suppression (checkbox) button.
|
||||
// NSAlert's suppression button is a non-bordered NSButton, unlike
|
||||
// push buttons which are bordered. This reliably identifies it
|
||||
// across macOS versions where the accessibility role may differ.
|
||||
NSMutableArray<NSView*>* cbStack =
|
||||
[NSMutableArray arrayWithObject:contentView];
|
||||
while ([cbStack count] > 0) {
|
||||
NSView* current = [cbStack lastObject];
|
||||
[cbStack removeLastObject];
|
||||
if ([current isKindOfClass:[NSButton class]]) {
|
||||
NSButton* btn = (NSButton*)current;
|
||||
if (![btn isBordered]) {
|
||||
NSString* title = [btn title];
|
||||
if (title && [title length] > 0) {
|
||||
info.checkbox_label = [title UTF8String];
|
||||
info.checkbox_checked =
|
||||
([btn state] == NSControlStateValueOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (NSView* sub in [current subviews]) {
|
||||
[cbStack addObject:sub];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
bool ClickMessageBoxButton(char* handle, size_t size, int button_index) {
|
||||
NSWindow* window = GetNSWindowFromHandle(handle, size);
|
||||
if (!window)
|
||||
return false;
|
||||
|
||||
NSWindow* sheet = [window attachedSheet];
|
||||
if (!sheet)
|
||||
return false;
|
||||
|
||||
// Find buttons in the sheet, sorted by tag.
|
||||
NSView* contentView = [sheet contentView];
|
||||
NSMutableArray<NSButton*>* buttons = [NSMutableArray array];
|
||||
|
||||
NSMutableArray<NSView*>* stack =
|
||||
[NSMutableArray arrayWithObject:contentView];
|
||||
while ([stack count] > 0) {
|
||||
NSView* current = [stack lastObject];
|
||||
[stack removeLastObject];
|
||||
if ([current isKindOfClass:[NSButton class]]) {
|
||||
NSButton* btn = (NSButton*)current;
|
||||
if ([btn bezelStyle] == NSBezelStyleRounded ||
|
||||
[btn bezelStyle] == NSBezelStyleRegularSquare) {
|
||||
[buttons addObject:btn];
|
||||
}
|
||||
}
|
||||
for (NSView* subview in [current subviews]) {
|
||||
[stack addObject:subview];
|
||||
}
|
||||
}
|
||||
|
||||
[buttons sortUsingComparator:^NSComparisonResult(NSButton* a, NSButton* b) {
|
||||
if ([a tag] < [b tag])
|
||||
return NSOrderedAscending;
|
||||
if ([a tag] > [b tag])
|
||||
return NSOrderedDescending;
|
||||
return NSOrderedSame;
|
||||
}];
|
||||
|
||||
if (button_index < 0 || button_index >= (int)[buttons count])
|
||||
return false;
|
||||
|
||||
NSButton* target = [buttons objectAtIndex:button_index];
|
||||
[target performClick:nil];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClickCheckbox(char* handle, size_t size) {
|
||||
NSWindow* window = GetNSWindowFromHandle(handle, size);
|
||||
if (!window)
|
||||
return false;
|
||||
|
||||
NSWindow* sheet = [window attachedSheet];
|
||||
if (!sheet)
|
||||
return false;
|
||||
|
||||
// Find the suppression/checkbox button — it is a non-bordered NSButton,
|
||||
// unlike the push buttons which are bordered.
|
||||
NSView* contentView = [sheet contentView];
|
||||
NSMutableArray<NSView*>* stack =
|
||||
[NSMutableArray arrayWithObject:contentView];
|
||||
while ([stack count] > 0) {
|
||||
NSView* current = [stack lastObject];
|
||||
[stack removeLastObject];
|
||||
if ([current isKindOfClass:[NSButton class]]) {
|
||||
NSButton* btn = (NSButton*)current;
|
||||
if (![btn isBordered] && [[btn title] length] > 0) {
|
||||
[btn performClick:nil];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (NSView* subview in [current subviews]) {
|
||||
[stack addObject:subview];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CancelFileDialog(char* handle, size_t size) {
|
||||
NSWindow* window = GetNSWindowFromHandle(handle, size);
|
||||
if (!window)
|
||||
return false;
|
||||
|
||||
NSWindow* sheet = [window attachedSheet];
|
||||
if (!sheet)
|
||||
return false;
|
||||
|
||||
// sheet is the NSSavePanel/NSOpenPanel window itself when presented as a
|
||||
// sheet. We need to find the actual panel object. On macOS, when an
|
||||
// NSSavePanel is run as a sheet, [window attachedSheet] returns the panel's
|
||||
// window. The panel can be retrieved because NSSavePanel IS the window.
|
||||
if ([sheet isKindOfClass:[NSSavePanel class]]) {
|
||||
NSSavePanel* panel = (NSSavePanel*)sheet;
|
||||
[panel cancel:nil];
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's not a recognized panel type, try ending the sheet directly.
|
||||
[NSApp endSheet:sheet returnCode:NSModalResponseCancel];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AcceptFileDialog(char* handle, size_t size, const std::string& filename) {
|
||||
NSWindow* window = GetNSWindowFromHandle(handle, size);
|
||||
if (!window)
|
||||
return false;
|
||||
|
||||
NSWindow* sheet = [window attachedSheet];
|
||||
if (!sheet)
|
||||
return false;
|
||||
|
||||
if (![sheet isKindOfClass:[NSSavePanel class]])
|
||||
return false;
|
||||
|
||||
NSSavePanel* panel = (NSSavePanel*)sheet;
|
||||
|
||||
// Set the filename if provided (for save dialogs).
|
||||
if (!filename.empty()) {
|
||||
NSString* name = [NSString stringWithUTF8String:filename.c_str()];
|
||||
[panel setNameFieldStringValue:name];
|
||||
// Resign first responder to commit the name field edit. Without this,
|
||||
// the panel may still use the previous value (e.g. "Untitled") when
|
||||
// the accept button is clicked immediately after.
|
||||
[sheet makeFirstResponder:nil];
|
||||
}
|
||||
|
||||
NSView* contentView = [sheet contentView];
|
||||
|
||||
// Search for the default button (key equivalent "\r") in the view hierarchy.
|
||||
NSMutableArray<NSView*>* stack =
|
||||
[NSMutableArray arrayWithObject:contentView];
|
||||
while ([stack count] > 0) {
|
||||
NSView* current = [stack lastObject];
|
||||
[stack removeLastObject];
|
||||
if ([current isKindOfClass:[NSButton class]]) {
|
||||
NSButton* btn = (NSButton*)current;
|
||||
if ([[btn keyEquivalent] isEqualToString:@"\r"]) {
|
||||
[btn performClick:nil];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (NSView* subview in [current subviews]) {
|
||||
[stack addObject:subview];
|
||||
}
|
||||
}
|
||||
|
||||
[NSApp endSheet:sheet returnCode:NSModalResponseOK];
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dialog_helper
|
||||
231
spec/fixtures/native-addon/dialog-helper/src/main.cc
vendored
Normal file
231
spec/fixtures/native-addon/dialog-helper/src/main.cc
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
#include <js_native_api.h>
|
||||
#include <node_api.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "dialog_helper.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper: extract (char* data, size_t length) from the first Buffer argument.
|
||||
bool GetHandleArg(napi_env env, napi_callback_info info, size_t expected_argc,
|
||||
napi_value* args, char** data, size_t* length) {
|
||||
size_t argc = expected_argc;
|
||||
napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
|
||||
if (status != napi_ok || argc < 1)
|
||||
return false;
|
||||
|
||||
bool is_buffer;
|
||||
status = napi_is_buffer(env, args[0], &is_buffer);
|
||||
if (status != napi_ok || !is_buffer) {
|
||||
napi_throw_error(env, NULL, "First argument must be a Buffer (native window handle)");
|
||||
return false;
|
||||
}
|
||||
|
||||
status = napi_get_buffer_info(env, args[0], (void**)data, length);
|
||||
return status == napi_ok;
|
||||
}
|
||||
|
||||
napi_value GetDialogInfo(napi_env env, napi_callback_info info) {
|
||||
napi_value args[1];
|
||||
char* data;
|
||||
size_t length;
|
||||
if (!GetHandleArg(env, info, 1, args, &data, &length))
|
||||
return NULL;
|
||||
|
||||
dialog_helper::DialogInfo di = dialog_helper::GetDialogInfo(data, length);
|
||||
|
||||
napi_value result;
|
||||
napi_create_object(env, &result);
|
||||
|
||||
// Message box properties
|
||||
napi_value type_val;
|
||||
napi_create_string_utf8(env, di.type.c_str(), di.type.size(), &type_val);
|
||||
napi_set_named_property(env, result, "type", type_val);
|
||||
|
||||
napi_value buttons_val;
|
||||
napi_create_string_utf8(env, di.buttons.c_str(), di.buttons.size(), &buttons_val);
|
||||
napi_set_named_property(env, result, "buttons", buttons_val);
|
||||
|
||||
napi_value message_val;
|
||||
napi_create_string_utf8(env, di.message.c_str(), di.message.size(), &message_val);
|
||||
napi_set_named_property(env, result, "message", message_val);
|
||||
|
||||
napi_value detail_val;
|
||||
napi_create_string_utf8(env, di.detail.c_str(), di.detail.size(), &detail_val);
|
||||
napi_set_named_property(env, result, "detail", detail_val);
|
||||
|
||||
napi_value checkbox_label_val;
|
||||
napi_create_string_utf8(env, di.checkbox_label.c_str(),
|
||||
di.checkbox_label.size(), &checkbox_label_val);
|
||||
napi_set_named_property(env, result, "checkboxLabel", checkbox_label_val);
|
||||
|
||||
napi_value checkbox_checked_val;
|
||||
napi_get_boolean(env, di.checkbox_checked, &checkbox_checked_val);
|
||||
napi_set_named_property(env, result, "checkboxChecked", checkbox_checked_val);
|
||||
|
||||
// File dialog properties
|
||||
napi_value prompt_val;
|
||||
napi_create_string_utf8(env, di.prompt.c_str(), di.prompt.size(), &prompt_val);
|
||||
napi_set_named_property(env, result, "prompt", prompt_val);
|
||||
|
||||
napi_value panel_message_val;
|
||||
napi_create_string_utf8(env, di.panel_message.c_str(),
|
||||
di.panel_message.size(), &panel_message_val);
|
||||
napi_set_named_property(env, result, "panelMessage", panel_message_val);
|
||||
|
||||
napi_value directory_val;
|
||||
napi_create_string_utf8(env, di.directory.c_str(), di.directory.size(),
|
||||
&directory_val);
|
||||
napi_set_named_property(env, result, "directory", directory_val);
|
||||
|
||||
// NSSavePanel-specific string/boolean properties
|
||||
napi_value name_field_label_val;
|
||||
napi_create_string_utf8(env, di.name_field_label.c_str(),
|
||||
di.name_field_label.size(), &name_field_label_val);
|
||||
napi_set_named_property(env, result, "nameFieldLabel", name_field_label_val);
|
||||
|
||||
napi_value name_field_value_val;
|
||||
napi_create_string_utf8(env, di.name_field_value.c_str(),
|
||||
di.name_field_value.size(), &name_field_value_val);
|
||||
napi_set_named_property(env, result, "nameFieldValue", name_field_value_val);
|
||||
|
||||
napi_value shows_tag_field_val;
|
||||
napi_get_boolean(env, di.shows_tag_field, &shows_tag_field_val);
|
||||
napi_set_named_property(env, result, "showsTagField", shows_tag_field_val);
|
||||
|
||||
// NSOpenPanel-specific properties
|
||||
napi_value can_choose_files_val;
|
||||
napi_get_boolean(env, di.can_choose_files, &can_choose_files_val);
|
||||
napi_set_named_property(env, result, "canChooseFiles", can_choose_files_val);
|
||||
|
||||
napi_value can_choose_dirs_val;
|
||||
napi_get_boolean(env, di.can_choose_directories, &can_choose_dirs_val);
|
||||
napi_set_named_property(env, result, "canChooseDirectories",
|
||||
can_choose_dirs_val);
|
||||
|
||||
napi_value allows_multi_val;
|
||||
napi_get_boolean(env, di.allows_multiple_selection, &allows_multi_val);
|
||||
napi_set_named_property(env, result, "allowsMultipleSelection",
|
||||
allows_multi_val);
|
||||
|
||||
// Shared panel properties (open and save)
|
||||
napi_value shows_hidden_val;
|
||||
napi_get_boolean(env, di.shows_hidden_files, &shows_hidden_val);
|
||||
napi_set_named_property(env, result, "showsHiddenFiles", shows_hidden_val);
|
||||
|
||||
napi_value resolves_aliases_val;
|
||||
napi_get_boolean(env, di.resolves_aliases, &resolves_aliases_val);
|
||||
napi_set_named_property(env, result, "resolvesAliases", resolves_aliases_val);
|
||||
|
||||
napi_value treats_packages_val;
|
||||
napi_get_boolean(env, di.treats_packages_as_directories, &treats_packages_val);
|
||||
napi_set_named_property(env, result, "treatsPackagesAsDirectories",
|
||||
treats_packages_val);
|
||||
|
||||
napi_value can_create_dirs_val;
|
||||
napi_get_boolean(env, di.can_create_directories, &can_create_dirs_val);
|
||||
napi_set_named_property(env, result, "canCreateDirectories",
|
||||
can_create_dirs_val);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value ClickMessageBoxButton(napi_env env, napi_callback_info info) {
|
||||
napi_value args[2];
|
||||
char* data;
|
||||
size_t length;
|
||||
if (!GetHandleArg(env, info, 2, args, &data, &length))
|
||||
return NULL;
|
||||
|
||||
int32_t button_index;
|
||||
napi_status status = napi_get_value_int32(env, args[1], &button_index);
|
||||
if (status != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Second argument must be a number (button index)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ok = dialog_helper::ClickMessageBoxButton(data, length, button_index);
|
||||
|
||||
napi_value result;
|
||||
napi_get_boolean(env, ok, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value ClickCheckbox(napi_env env, napi_callback_info info) {
|
||||
napi_value args[1];
|
||||
char* data;
|
||||
size_t length;
|
||||
if (!GetHandleArg(env, info, 1, args, &data, &length))
|
||||
return NULL;
|
||||
|
||||
bool ok = dialog_helper::ClickCheckbox(data, length);
|
||||
|
||||
napi_value result;
|
||||
napi_get_boolean(env, ok, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value CancelFileDialog(napi_env env, napi_callback_info info) {
|
||||
napi_value args[1];
|
||||
char* data;
|
||||
size_t length;
|
||||
if (!GetHandleArg(env, info, 1, args, &data, &length))
|
||||
return NULL;
|
||||
|
||||
bool ok = dialog_helper::CancelFileDialog(data, length);
|
||||
|
||||
napi_value result;
|
||||
napi_get_boolean(env, ok, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value AcceptFileDialog(napi_env env, napi_callback_info info) {
|
||||
napi_value args[2];
|
||||
char* data;
|
||||
size_t length;
|
||||
if (!GetHandleArg(env, info, 2, args, &data, &length))
|
||||
return NULL;
|
||||
|
||||
std::string filename;
|
||||
// Second argument (filename) is optional.
|
||||
napi_valuetype vtype;
|
||||
napi_typeof(env, args[1], &vtype);
|
||||
if (vtype == napi_string) {
|
||||
size_t str_len;
|
||||
napi_get_value_string_utf8(env, args[1], NULL, 0, &str_len);
|
||||
filename.resize(str_len);
|
||||
napi_get_value_string_utf8(env, args[1], &filename[0], str_len + 1,
|
||||
&str_len);
|
||||
}
|
||||
|
||||
bool ok = dialog_helper::AcceptFileDialog(data, length, filename);
|
||||
|
||||
napi_value result;
|
||||
napi_get_boolean(env, ok, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor descriptors[] = {
|
||||
{"getDialogInfo", NULL, GetDialogInfo, NULL, NULL, NULL,
|
||||
napi_enumerable, NULL},
|
||||
{"clickMessageBoxButton", NULL, ClickMessageBoxButton, NULL, NULL, NULL,
|
||||
napi_enumerable, NULL},
|
||||
{"clickCheckbox", NULL, ClickCheckbox, NULL, NULL, NULL,
|
||||
napi_enumerable, NULL},
|
||||
{"cancelFileDialog", NULL, CancelFileDialog, NULL, NULL, NULL,
|
||||
napi_enumerable, NULL},
|
||||
{"acceptFileDialog", NULL, AcceptFileDialog, NULL, NULL, NULL,
|
||||
napi_enumerable, NULL},
|
||||
};
|
||||
|
||||
napi_define_properties(env, exports,
|
||||
sizeof(descriptors) / sizeof(*descriptors),
|
||||
descriptors);
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
@@ -186,6 +186,39 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
await once(browserWindow.webContents, 'did-create-window');
|
||||
});
|
||||
|
||||
it('reuses an existing window when window.open is called with the same frame name', async () => {
|
||||
let handlerCallCount = 0;
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
handlerCallCount++;
|
||||
return { action: 'allow' };
|
||||
});
|
||||
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
|
||||
await browserWindow.webContents.executeJavaScript("window.open('about:blank?one', 'named-target', 'show=no') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
expect(handlerCallCount).to.equal(1);
|
||||
expect(childWindow.webContents.getURL()).to.equal('about:blank?one');
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not fire when reusing a named window');
|
||||
});
|
||||
|
||||
const didNavigate = once(childWindow.webContents, 'did-navigate');
|
||||
const sameWindow = await browserWindow.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const first = window.open('about:blank?one', 'named-target', 'show=no');
|
||||
const second = window.open('about:blank?two', 'named-target', 'show=no');
|
||||
return first === second;
|
||||
})()
|
||||
`);
|
||||
await didNavigate;
|
||||
|
||||
expect(sameWindow).to.be.true('window.open with matching frame name should return the same window proxy');
|
||||
expect(handlerCallCount).to.equal(1, 'setWindowOpenHandler should not be called when Blink resolves the named target');
|
||||
expect(childWindow.webContents.getURL()).to.equal('about:blank?two');
|
||||
expect(BrowserWindow.getAllWindows()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('can change webPreferences of child windows', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"node-gyp-install": "node-gyp install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-ci/dialog-helper": "*",
|
||||
"@electron-ci/echo": "*",
|
||||
"@electron-ci/external-ab": "*",
|
||||
"@electron-ci/is-valid-window": "*",
|
||||
|
||||
@@ -647,6 +647,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@electron-ci/dialog-helper@npm:*, @electron-ci/dialog-helper@workspace:spec/fixtures/native-addon/dialog-helper":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@electron-ci/dialog-helper@workspace:spec/fixtures/native-addon/dialog-helper"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@electron-ci/echo@npm:*, @electron-ci/echo@workspace:spec/fixtures/native-addon/echo":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@electron-ci/echo@workspace:spec/fixtures/native-addon/echo"
|
||||
@@ -4899,6 +4905,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "electron-test-main@workspace:spec"
|
||||
dependencies:
|
||||
"@electron-ci/dialog-helper": "npm:*"
|
||||
"@electron-ci/echo": "npm:*"
|
||||
"@electron-ci/external-ab": "npm:*"
|
||||
"@electron-ci/is-valid-window": "npm:*"
|
||||
|
||||
Reference in New Issue
Block a user