[Windows] Run native file dialogs in thread to make it non-blocking.

This commit is contained in:
bruvzg 2024-07-24 12:57:46 +03:00
parent 1bd740d18d
commit 66832e9968
2 changed files with 269 additions and 83 deletions

View file

@ -250,6 +250,14 @@ void DisplayServerWindows::tts_stop() {
tts->stop();
}
Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
}
Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
}
// Silence warning due to a COM API weirdness.
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
@ -377,22 +385,85 @@ public:
#pragma GCC diagnostic pop
#endif
Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
LRESULT CALLBACK WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DisplayServerWindows *ds_win = static_cast<DisplayServerWindows *>(DisplayServer::get_singleton());
if (ds_win) {
return ds_win->WndProcFileDialog(hWnd, uMsg, wParam, lParam);
} else {
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}
}
Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
LRESULT DisplayServerWindows::WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
MutexLock lock(file_dialog_mutex);
if (file_dialog_wnd.has(hWnd)) {
if (file_dialog_wnd[hWnd]->close_requested.is_set()) {
IPropertyStore *prop_store;
HRESULT hr = SHGetPropertyStoreForWindow(hWnd, IID_IPropertyStore, (void **)&prop_store);
if (hr == S_OK) {
PROPVARIANT val;
PropVariantInit(&val);
prop_store->SetValue(PKEY_AppUserModel_ID, val);
prop_store->Release();
}
DestroyWindow(hWnd);
file_dialog_wnd.erase(hWnd);
}
}
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}
Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
_THREAD_SAFE_METHOD_
void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
DisplayServerWindows *ds = static_cast<DisplayServerWindows *>(get_singleton());
FileDialogData *fd = (FileDialogData *)p_ud;
ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
if (fd->mode < 0 && fd->mode >= DisplayServer::FILE_DIALOG_MODE_SAVE_MAX) {
fd->finished.set();
return;
}
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
int64_t x = fd->wrect.position.x;
int64_t y = fd->wrect.position.y;
int64_t w = fd->wrect.size.x;
int64_t h = fd->wrect.size.y;
WNDCLASSW wc = {};
wc.lpfnWndProc = (WNDPROC)::WndProcFileDialog;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = L"Engine File Dialog";
RegisterClassW(&wc);
HWND hwnd_dialog = CreateWindowExW(WS_EX_APPWINDOW, L"Engine File Dialog", L"", WS_OVERLAPPEDWINDOW, x, y, w, h, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
if (hwnd_dialog) {
{
MutexLock lock(ds->file_dialog_mutex);
ds->file_dialog_wnd[hwnd_dialog] = fd;
}
HICON mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_SMALL, 0);
if (mainwindow_icon) {
SendMessage(hwnd_dialog, WM_SETICON, ICON_SMALL, (LPARAM)mainwindow_icon);
}
mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_BIG, 0);
if (mainwindow_icon) {
SendMessage(hwnd_dialog, WM_SETICON, ICON_BIG, (LPARAM)mainwindow_icon);
}
IPropertyStore *prop_store;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store);
if (hr == S_OK) {
PROPVARIANT val;
InitPropVariantFromString((PCWSTR)fd->appid.utf16().get_data(), &val);
prop_store->SetValue(PKEY_AppUserModel_ID, val);
prop_store->Release();
}
}
SetCurrentProcessExplicitAppUserModelID((PCWSTR)fd->appid.utf16().get_data());
Vector<Char16String> filter_names;
Vector<Char16String> filter_exts;
for (const String &E : p_filters) {
for (const String &E : fd->filters) {
Vector<String> tokens = E.split(";");
if (tokens.size() >= 1) {
String flt = tokens[0].strip_edges();
@ -425,11 +496,9 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
filters.push_back({ (LPCWSTR)filter_names[i].ptr(), (LPCWSTR)filter_exts[i].ptr() });
}
WindowID prev_focus = last_focused_window;
HRESULT hr = S_OK;
IFileDialog *pfd = nullptr;
if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) {
if (fd->mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {
hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileSaveDialog, (void **)&pfd);
} else {
hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd);
@ -445,40 +514,32 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
IFileDialogCustomize *pfdc = nullptr;
hr = pfd->QueryInterface(IID_PPV_ARGS(&pfdc));
for (int i = 0; i < p_options.size(); i++) {
const Dictionary &item = p_options[i];
for (int i = 0; i < fd->options.size(); i++) {
const Dictionary &item = fd->options[i];
if (!item.has("name") || !item.has("values") || !item.has("default")) {
continue;
}
const String &name = item["name"];
const Vector<String> &options = item["values"];
int default_idx = item["default"];
event_handler->add_option(pfdc, name, options, default_idx);
event_handler->add_option(pfdc, item["name"], item["values"], item["default_idx"]);
}
event_handler->set_root(p_root);
event_handler->set_root(fd->root);
pfdc->Release();
DWORD flags;
pfd->GetOptions(&flags);
if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) {
flags |= FOS_ALLOWMULTISELECT;
}
if (p_mode == FILE_DIALOG_MODE_OPEN_DIR) {
if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR) {
flags |= FOS_PICKFOLDERS;
}
if (p_show_hidden) {
if (fd->show_hidden) {
flags |= FOS_FORCESHOWHIDDEN;
}
pfd->SetOptions(flags | FOS_FORCEFILESYSTEM);
pfd->SetTitle((LPCWSTR)p_title.utf16().ptr());
pfd->SetTitle((LPCWSTR)fd->title.utf16().ptr());
String dir = ProjectSettings::get_singleton()->globalize_path(p_current_directory);
if (dir == ".") {
dir = OS::get_singleton()->get_executable_path().get_base_dir();
}
dir = dir.replace("/", "\\");
String dir = fd->current_directory.replace("/", "\\");
IShellItem *shellitem = nullptr;
hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem);
@ -487,16 +548,11 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
pfd->SetFolder(shellitem);
}
pfd->SetFileName((LPCWSTR)p_filename.utf16().ptr());
pfd->SetFileName((LPCWSTR)fd->filename.utf16().ptr());
pfd->SetFileTypes(filters.size(), filters.ptr());
pfd->SetFileTypeIndex(0);
WindowID window_id = _get_focused_window_or_popup();
if (!windows.has(window_id)) {
window_id = MAIN_WINDOW_ID;
}
hr = pfd->Show(windows[window_id].hWnd);
hr = pfd->Show(hwnd_dialog);
pfd->Unadvise(cookie);
Dictionary options = event_handler->get_selected();
@ -513,7 +569,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
if (SUCCEEDED(hr)) {
Vector<String> file_names;
if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) {
IShellItemArray *results;
hr = static_cast<IFileOpenDialog *>(pfd)->GetResults(&results);
if (SUCCEEDED(hr)) {
@ -546,73 +602,148 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
result->Release();
}
}
if (p_callback.is_valid()) {
if (p_options_in_cb) {
if (fd->callback.is_valid()) {
if (fd->options_in_cb) {
Variant v_result = true;
Variant v_files = file_names;
Variant v_index = index;
Variant v_opt = options;
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
p_callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
}
fd->callback.call_deferredp(cb_args, 4);
} else {
Variant v_result = true;
Variant v_files = file_names;
Variant v_index = index;
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
p_callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
}
fd->callback.call_deferredp(cb_args, 3);
}
}
} else {
if (p_callback.is_valid()) {
if (p_options_in_cb) {
if (fd->callback.is_valid()) {
if (fd->options_in_cb) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = index;
Variant v_opt = options;
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
Variant v_index = 0;
Variant v_opt = Dictionary();
const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
p_callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
}
fd->callback.call_deferredp(cb_args, 4);
} else {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = index;
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
Variant v_index = 0;
const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
p_callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
}
fd->callback.call_deferredp(cb_args, 3);
}
}
}
pfd->Release();
if (prev_focus != INVALID_WINDOW_ID) {
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus);
}
return OK;
} else {
return ERR_CANT_OPEN;
if (fd->callback.is_valid()) {
if (fd->options_in_cb) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = 0;
Variant v_opt = Dictionary();
const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
fd->callback.call_deferredp(cb_args, 4);
} else {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = 0;
const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
fd->callback.call_deferredp(cb_args, 3);
}
}
}
{
MutexLock lock(ds->file_dialog_mutex);
if (hwnd_dialog && ds->file_dialog_wnd.has(hwnd_dialog)) {
IPropertyStore *prop_store;
hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store);
if (hr == S_OK) {
PROPVARIANT val;
PropVariantInit(&val);
prop_store->SetValue(PKEY_AppUserModel_ID, val);
prop_store->Release();
}
DestroyWindow(hwnd_dialog);
ds->file_dialog_wnd.erase(hwnd_dialog);
}
}
UnregisterClassW(L"Engine File Dialog", GetModuleHandle(nullptr));
CoUninitialize();
fd->finished.set();
if (fd->window_id != INVALID_WINDOW_ID) {
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd->window_id);
}
}
Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
_THREAD_SAFE_METHOD_
ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
WindowID window_id = _get_focused_window_or_popup();
if (!windows.has(window_id)) {
window_id = MAIN_WINDOW_ID;
}
String appname;
if (Engine::get_singleton()->is_editor_hint()) {
appname = "Godot.GodotEditor." + String(VERSION_BRANCH);
} else {
String name = GLOBAL_GET("application/config/name");
String version = GLOBAL_GET("application/config/version");
if (version.is_empty()) {
version = "0";
}
String clean_app_name = name.to_pascal_case();
for (int i = 0; i < clean_app_name.length(); i++) {
if (!is_ascii_alphanumeric_char(clean_app_name[i]) && clean_app_name[i] != '_' && clean_app_name[i] != '.') {
clean_app_name[i] = '_';
}
}
clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix(".");
appname = "Godot." + clean_app_name + "." + version;
}
FileDialogData *fd = memnew(FileDialogData);
if (window_id != INVALID_WINDOW_ID) {
fd->hwnd_owner = windows[window_id].hWnd;
RECT crect;
GetWindowRect(fd->hwnd_owner, &crect);
fd->wrect = Rect2i(crect.left, crect.top, crect.right - crect.left, crect.bottom - crect.top);
} else {
fd->hwnd_owner = 0;
fd->wrect = Rect2i(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT);
}
fd->appid = appname;
fd->title = p_title;
fd->current_directory = p_current_directory;
fd->root = p_root;
fd->filename = p_filename;
fd->show_hidden = p_show_hidden;
fd->mode = p_mode;
fd->window_id = window_id;
fd->filters = p_filters;
fd->options = p_options;
fd->callback = p_callback;
fd->options_in_cb = p_options_in_cb;
fd->finished.clear();
fd->close_requested.clear();
fd->listener_thread.start(DisplayServerWindows::_thread_fd_monitor, fd);
file_dialogs.push_back(fd);
return OK;
}
void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) {
@ -3022,6 +3153,21 @@ void DisplayServerWindows::process_events() {
_process_key_events();
Input::get_singleton()->flush_buffered_events();
}
LocalVector<List<FileDialogData *>::Element *> to_remove;
for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) {
FileDialogData *fd = E->get();
if (fd->finished.is_set()) {
if (fd->listener_thread.is_started()) {
fd->listener_thread.wait_to_finish();
}
to_remove.push_back(E);
}
}
for (List<FileDialogData *>::Element *E : to_remove) {
memdelete(E->get());
E->erase();
}
}
void DisplayServerWindows::force_process_and_drop_events() {
@ -5703,12 +5849,6 @@ Vector2i _get_device_ids(const String &p_device_name) {
return ids;
}
typedef enum _SHC_PROCESS_DPI_AWARENESS {
SHC_PROCESS_DPI_UNAWARE = 0,
SHC_PROCESS_SYSTEM_DPI_AWARE = 1,
SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2
} SHC_PROCESS_DPI_AWARENESS;
bool DisplayServerWindows::is_dark_mode_supported() const {
return ux_theme_available;
}
@ -6260,6 +6400,20 @@ void DisplayServerWindows::register_windows_driver() {
}
DisplayServerWindows::~DisplayServerWindows() {
LocalVector<List<FileDialogData *>::Element *> to_remove;
for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) {
FileDialogData *fd = E->get();
if (fd->listener_thread.is_started()) {
fd->close_requested.set();
fd->listener_thread.wait_to_finish();
}
to_remove.push_back(E);
}
for (List<FileDialogData *>::Element *E : to_remove) {
memdelete(E->get());
E->erase();
}
delete joypad;
touch_state.clear();

View file

@ -350,6 +350,12 @@ typedef struct {
ICONDIRENTRY idEntries[1]; // An entry for each image (idCount of 'em)
} ICONDIR, *LPICONDIR;
typedef enum _SHC_PROCESS_DPI_AWARENESS {
SHC_PROCESS_DPI_UNAWARE = 0,
SHC_PROCESS_SYSTEM_DPI_AWARE = 1,
SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2,
} SHC_PROCESS_DPI_AWARENESS;
class DisplayServerWindows : public DisplayServer {
// No need to register with GDCLASS, it's platform-specific and nothing is added.
@ -544,6 +550,31 @@ class DisplayServerWindows : public DisplayServer {
IndicatorID indicator_id_counter = 0;
HashMap<IndicatorID, IndicatorData> indicators;
struct FileDialogData {
HWND hwnd_owner = 0;
Rect2i wrect;
String appid;
String title;
String current_directory;
String root;
String filename;
bool show_hidden = false;
DisplayServer::FileDialogMode mode = FileDialogMode::FILE_DIALOG_MODE_OPEN_ANY;
Vector<String> filters;
TypedArray<Dictionary> options;
WindowID window_id = DisplayServer::INVALID_WINDOW_ID;
Callable callback;
bool options_in_cb = false;
Thread listener_thread;
SafeFlag close_requested;
SafeFlag finished;
};
Mutex file_dialog_mutex;
List<FileDialogData *> file_dialogs;
HashMap<HWND, FileDialogData *> file_dialog_wnd;
static void _thread_fd_monitor(void *p_ud);
HashMap<int64_t, MouseButton> pointer_prev_button;
HashMap<int64_t, MouseButton> pointer_button;
HashMap<int64_t, LONG> pointer_down_time;
@ -605,6 +636,7 @@ class DisplayServerWindows : public DisplayServer {
String _get_klid(HKL p_hkl) const;
public:
LRESULT WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT MouseProc(int code, WPARAM wParam, LPARAM lParam);