Compare commits

..

2 Commits

Author SHA1 Message Date
Shelley Vohr
c203d4c070 chore: improve breaking change description 2026-03-13 11:55:47 +01:00
Shelley Vohr
74c1811b35 fix: use Downloads folder as default path for file dialogs
Co-authored-by: Sourav Bera <sbera987654321@gmail.com>
2026-03-13 11:49:23 +01:00
9 changed files with 98 additions and 64 deletions

View File

@@ -28,7 +28,10 @@ added:
* `window` [BaseWindow](base-window.md) (optional)
* `options` Object
* `title` string (optional)
* `defaultPath` string (optional)
* `defaultPath` string (optional) - Absolute directory path, absolute file
path, or file name to use by default. If not provided, the dialog will
default to the user's Downloads folder, or their home directory if Downloads
doesn't exist.
* `buttonLabel` string (optional) - Custom label for the confirmation button, when
left empty the default label will be used.
* `filters` [FileFilter[]](structures/file-filter.md) (optional)
@@ -109,7 +112,10 @@ changes:
* `window` [BaseWindow](base-window.md) (optional)
* `options` Object
* `title` string (optional)
* `defaultPath` string (optional)
* `defaultPath` string (optional) - Absolute directory path, absolute file
path, or file name to use by default. If not provided, the dialog will
default to the user's Downloads folder, or their home directory if Downloads
doesn't exist.
* `buttonLabel` string (optional) - Custom label for the confirmation button, when
left empty the default label will be used.
* `filters` [FileFilter[]](structures/file-filter.md) (optional)
@@ -198,7 +204,9 @@ added:
* `options` Object
* `title` string (optional) - The dialog title. Cannot be displayed on some _Linux_ desktop environments.
* `defaultPath` string (optional) - Absolute directory path, absolute file
path, or file name to use by default.
path, or file name to use by default. If not provided, the dialog will
default to the user's Downloads folder, or their home directory if Downloads
doesn't exist.
* `buttonLabel` string (optional) - Custom label for the confirmation button, when
left empty the default label will be used.
* `filters` [FileFilter[]](structures/file-filter.md) (optional)
@@ -238,7 +246,9 @@ changes:
* `options` Object
* `title` string (optional) - The dialog title. Cannot be displayed on some _Linux_ desktop environments.
* `defaultPath` string (optional) - Absolute directory path, absolute file
path, or file name to use by default.
path, or file name to use by default. If not provided, the dialog will
default to the user's Downloads folder, or their home directory if Downloads
doesn't exist.
* `buttonLabel` string (optional) - Custom label for the confirmation button, when
left empty the default label will be used.
* `filters` [FileFilter[]](structures/file-filter.md) (optional)

View File

@@ -12,6 +12,34 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron.
## Planned Breaking API Changes (43.0)
### Behavior Changed: Dialog methods default to Downloads directory
The `defaultPath` option for the following methods now defaults to the user's Downloads folder (or their home directory if Downloads doesn't exist) when not explicitly provided:
* `dialog.showOpenDialog`
* `dialog.showOpenDialogSync`
* `dialog.showSaveDialog`
* `dialog.showSaveDialogSync`
Previously, when no `defaultPath` was provided, the underlying OS file dialog would determine the initial directory — typically remembering the last directory the user navigated to, or falling back to an OS-specific default. Now, Electron explicitly sets the initial directory to Downloads, which also means the OS will no longer track and restore the last-used directory between dialog invocations.
To preserve the old behavior, you can track the last-used directory yourself and pass it as `defaultPath`:
```js
const path = require('node:path')
let lastUsedPath
const result = await dialog.showOpenDialog({
defaultPath: lastUsedPath
})
if (!result.canceled && result.filePaths.length > 0) {
lastUsedPath = path.dirname(result.filePaths[0])
}
```
## Planned Breaking API Changes (42.0)
### Behavior Changed: macOS notifications now use `UNNotification` API

View File

@@ -13,6 +13,7 @@
#include "shell/browser/javascript_environment.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/file_dialog.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
@@ -81,14 +82,18 @@ class FileChooserDialog : public ui::SelectFileDialog::Listener {
ui::SelectFileDialog::FileTypeInfo file_info =
GetFilterInfo(settings.filters);
ApplySettings(settings);
dialog_->SelectFile(
ui::SelectFileDialog::SELECT_SAVEAS_FILE,
base::UTF8ToUTF16(settings.title), settings.default_path,
&file_info /* file_types */, 0 /* file_type_index */,
base::FilePath::StringType() /* default_extension */,
settings.parent_window ? settings.parent_window->GetNativeWindow()
: nullptr,
nullptr);
base::FilePath default_path = settings.default_path.empty()
? electron::GetDefaultPath()
: settings.default_path;
dialog_->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
base::UTF8ToUTF16(settings.title), default_path,
&file_info /* file_types */, 0 /* file_type_index */,
base::FilePath::StringType() /* default_extension */,
settings.parent_window
? settings.parent_window->GetNativeWindow()
: nullptr,
nullptr);
}
void RunSaveDialog(gin_helper::Promise<gin_helper::Dictionary> promise,
@@ -108,9 +113,13 @@ class FileChooserDialog : public ui::SelectFileDialog::Listener {
ui::SelectFileDialog::FileTypeInfo file_info =
GetFilterInfo(settings.filters);
ApplySettings(settings);
base::FilePath default_path = settings.default_path.empty()
? electron::GetDefaultPath()
: settings.default_path;
dialog_->SelectFile(
GetDialogType(settings.properties), base::UTF8ToUTF16(settings.title),
settings.default_path, &file_info, 0 /* file_type_index */,
default_path, &file_info, 0 /* file_type_index */,
base::FilePath::StringType() /* default_extension */,
settings.parent_window ? settings.parent_window->GetNativeWindow()
: nullptr,

View File

@@ -21,6 +21,7 @@
#include "content/public/browser/browser_thread.h"
#include "electron/mas.h"
#include "shell/browser/native_window.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
@@ -186,14 +187,18 @@ void SetupDialog(NSSavePanel* dialog, const DialogSettings& settings) {
[dialog setShowsTagField:settings.shows_tag_field];
base::FilePath default_path = settings.default_path.empty()
? electron::GetDefaultPath()
: settings.default_path;
NSString* default_dir = nil;
NSString* default_filename = nil;
if (!settings.default_path.empty()) {
if (!default_path.empty()) {
electron::ScopedAllowBlockingForElectron allow_blocking;
if (base::DirectoryExists(settings.default_path)) {
default_dir = base::SysUTF8ToNSString(settings.default_path.value());
if (base::DirectoryExists(default_path)) {
default_dir = base::SysUTF8ToNSString(default_path.value());
} else {
if (settings.default_path.IsAbsolute()) {
if (default_path.IsAbsolute()) {
default_dir =
base::SysUTF8ToNSString(settings.default_path.DirName().value());
}

View File

@@ -21,6 +21,7 @@
#include "base/win/registry.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/win/dialog_thread.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
@@ -106,8 +107,12 @@ static HRESULT ShowFileDialog(IFileDialog* dialog,
static void ApplySettings(IFileDialog* dialog, const DialogSettings& settings) {
std::wstring file_part;
if (!IsDirectory(settings.default_path))
file_part = settings.default_path.BaseName().value();
base::FilePath default_path = settings.default_path.empty()
? electron::GetDefaultPath()
: settings.default_path;
if (!IsDirectory(default_path))
file_part = default_path.BaseName().value();
dialog->SetFileName(file_part.c_str());
@@ -149,8 +154,8 @@ static void ApplySettings(IFileDialog* dialog, const DialogSettings& settings) {
}
}
if (settings.default_path.IsAbsolute()) {
SetDefaultFolder(dialog, settings.default_path);
if (default_path.IsAbsolute()) {
SetDefaultFolder(dialog, default_path);
}
}

View File

@@ -4,8 +4,6 @@
#include "shell/browser/ui/win/electron_desktop_window_tree_host_win.h"
#include <memory>
#include "base/win/windows_version.h"
#include "electron/buildflags/buildflags.h"
#include "shell/browser/api/electron_api_web_contents.h"
@@ -13,7 +11,6 @@
#include "shell/browser/ui/views/win_frame_view.h"
#include "shell/browser/win/dark_mode.h"
#include "ui/base/win/hwnd_metrics.h"
#include "ui/events/keycodes/keyboard_codes.h"
namespace electron {
@@ -153,25 +150,6 @@ bool ElectronDesktopWindowTreeHostWin::HandleMouseEvent(ui::MouseEvent* event) {
return views::DesktopWindowTreeHostWin::HandleMouseEvent(event);
}
void ElectronDesktopWindowTreeHostWin::HandleKeyEvent(ui::KeyEvent* event) {
// Chromium's base implementation intentionally drops ALT+SPACE keydown
// events so that the subsequent WM_SYSCHAR can open the system menu.
// We defer the event instead, so that if the system-context-menu event
// is prevented by the user, we can replay it to the renderer.
if ((event->type() == ui::EventType::kKeyPressed) &&
(event->key_code() == ui::VKEY_SPACE) &&
(event->flags() & ui::EF_ALT_DOWN) &&
!(event->flags() & ui::EF_CONTROL_DOWN)) {
if (views::Widget* widget = GetWidget();
widget && widget->non_client_view()) {
deferred_alt_space_event_ = std::make_unique<ui::KeyEvent>(*event);
return;
}
}
views::DesktopWindowTreeHostWin::HandleKeyEvent(event);
}
bool ElectronDesktopWindowTreeHostWin::HandleIMEMessage(UINT message,
WPARAM w_param,
LPARAM l_param,
@@ -187,20 +165,8 @@ bool ElectronDesktopWindowTreeHostWin::HandleIMEMessage(UINT message,
native_window_view_->NotifyWindowSystemContextMenu(
location.x(), location.y(), &prevent_default);
if (prevent_default) {
// The system menu was suppressed. Replay the deferred ALT+SPACE
// keydown so it reaches before-input-event and the renderer.
if (deferred_alt_space_event_) {
SendEventToSink(deferred_alt_space_event_.get());
deferred_alt_space_event_.reset();
}
return true;
}
// System menu not prevented — discard the deferred event and let
// the default WM_SYSCHAR handling open the system menu.
deferred_alt_space_event_.reset();
return views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
return prevent_default ||
views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
l_param, result);
}
}

View File

@@ -7,10 +7,8 @@
#include <windows.h>
#include <memory>
#include <optional>
#include "ui/events/event.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_win.h"
namespace electron {
@@ -46,7 +44,6 @@ class ElectronDesktopWindowTreeHostWin : public views::DesktopWindowTreeHostWin,
int frame_thickness) const override;
bool HandleMouseEventForCaption(UINT message) const override;
bool HandleMouseEvent(ui::MouseEvent* event) override;
void HandleKeyEvent(ui::KeyEvent* event) override;
bool HandleIMEMessage(UINT message,
WPARAM w_param,
LPARAM l_param,
@@ -66,10 +63,6 @@ class ElectronDesktopWindowTreeHostWin : public views::DesktopWindowTreeHostWin,
std::optional<bool> force_should_paint_as_active_;
bool allow_screenshots_ = true;
bool widget_init_done_ = false;
// Stores the ALT+SPACE key event deferred from HandleKeyEvent so it can be
// replayed in HandleIMEMessage if the system-context-menu is prevented.
std::unique_ptr<ui::KeyEvent> deferred_alt_space_event_;
};
} // namespace electron

View File

@@ -114,4 +114,18 @@ void RegisterPathProvider() {
PATH_END);
}
base::FilePath GetDefaultPath() {
base::FilePath path;
ScopedAllowBlockingForElectron allow_blocking;
if (base::PathService::Get(chrome::DIR_DEFAULT_DOWNLOADS, &path) &&
base::DirectoryExists(path))
return path;
if (base::PathService::Get(base::DIR_HOME, &path))
return path;
return base::FilePath();
}
} // namespace electron

View File

@@ -51,6 +51,10 @@ static_assert(PATH_START < PATH_END, "invalid PATH boundaries");
// Register the path provider with the base::PathService.
void RegisterPathProvider();
// Returns a default directory for file dialogs when no default path is
// provided.
base::FilePath GetDefaultPath();
} // namespace electron
#endif // ELECTRON_SHELL_COMMON_ELECTRON_PATHS_H_