diff --git a/wpigui/src/main/native/cpp/portable-file-dialogs.cpp b/wpigui/src/main/native/cpp/portable-file-dialogs.cpp index 0d9447558d..2e2ac7becf 100644 --- a/wpigui/src/main/native/cpp/portable-file-dialogs.cpp +++ b/wpigui/src/main/native/cpp/portable-file-dialogs.cpp @@ -5,7 +5,7 @@ // // Portable File Dialogs // -// Copyright © 2018—2020 Sam Hocevar +// Copyright © 2018—2022 Sam Hocevar // // This library is free software. It comes without any warranty, to // the extent permitted by applicable law. You can redistribute it @@ -20,13 +20,15 @@ #ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN 1 #endif -#include +#pragma comment(lib, "Advapi32.lib") +#include #include -#include -#include // IFileDialog +#include +#include // IFileDialog #include #include #include // std::async +#include // GetUserProfileDirectory() #elif __EMSCRIPTEN__ #include @@ -43,9 +45,11 @@ #include // popen() #include // std::getenv() #include // fcntl() -#include // read(), pipe(), dup2() +#include // read(), pipe(), dup2(), getuid() #include // ::kill, std::signal +#include // stat() #include // waitpid() +#include // getpwnam() #endif #ifdef _WIN32 @@ -91,7 +95,7 @@ public: { public: proc(dll const &lib, std::string const &sym) - : m_proc(reinterpret_cast(::GetProcAddress(lib.handle, sym.c_str()))) + : m_proc(reinterpret_cast((void *)::GetProcAddress(lib.handle, sym.c_str()))) {} operator bool() const { return m_proc != nullptr; } @@ -175,6 +179,7 @@ private: #endif }; + // internal free functions implementations #if _WIN32 @@ -227,8 +232,55 @@ static inline bool starts_with(std::string const &str, std::string const &prefix str.compare(0, prefix.size(), prefix) == 0; } +// This is necessary until C++17 which will have std::filesystem::is_directory + +static inline bool is_directory(std::string const &path) +{ +#if _WIN32 + auto attr = GetFileAttributesA(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY); +#elif __EMSCRIPTEN__ + // TODO + return false; +#else + struct stat s; + return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode); +#endif +} + +// This is necessary because getenv is not thread-safe + +static inline std::string getenv(std::string const &str) +{ +#if _WIN32 + char *buf = nullptr; + size_t size = 0; + if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf) + { + std::string ret(buf); + free(buf); + return ret; + } + return ""; +#else + auto buf = std::getenv(str.c_str()); + return buf ? buf : ""; +#endif +} + } // namespace internal +// +// The path class provides some platform-specific path constants +// + +class path : protected internal::platform +{ +public: + static std::string home(); + static std::string separator(); +}; + // settings implementation settings::settings(bool resync) @@ -237,6 +289,11 @@ settings::settings(bool resync) if (flags(flag::is_scanned)) return; + + auto pfd_verbose = internal::getenv("PFD_VERBOSE"); + auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase); + if (!std::regex_match(pfd_verbose, match_no)) + flags(flag::is_verbose) = true; #if _WIN32 flags(flag::is_vista) = internal::is_vista(); @@ -249,10 +306,10 @@ settings::settings(bool resync) // If multiple helpers are available, try to default to the best one if (flags(flag::has_zenity) && flags(flag::has_kdialog)) { - auto desktop_name = std::getenv("XDG_SESSION_DESKTOP"); - if (desktop_name && desktop_name == std::string("gnome")) + auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name == std::string("gnome")) flags(flag::has_kdialog) = false; - else if (desktop_name && desktop_name == std::string("KDE")) + else if (desktop_name == std::string("KDE")) flags(flag::has_zenity) = false; } #endif @@ -314,6 +371,59 @@ bool &settings::flags(flag in_flag) return const_cast(static_cast(this)->flags(in_flag)); } +// path implementation +std::string path::home() +{ +#if _WIN32 + // First try the USERPROFILE environment variable + auto user_profile = internal::getenv("USERPROFILE"); + if (user_profile.size() > 0) + return user_profile; + // Otherwise, try GetUserProfileDirectory() + HANDLE token = nullptr; + DWORD len = MAX_PATH; + char buf[MAX_PATH] = { '\0' }; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + { + dll userenv("userenv.dll"); + dll::proc get_user_profile_directory(userenv, "GetUserProfileDirectoryA"); + get_user_profile_directory(token, buf, &len); + CloseHandle(token); + if (*buf) + return buf; + } +#elif __EMSCRIPTEN__ + return "/"; +#else + // First try the HOME environment variable + auto home = internal::getenv("HOME"); + if (home.size() > 0) + return home; + // Otherwise, try getpwuid_r() + size_t len = 4096; +#if defined(_SC_GETPW_R_SIZE_MAX) + auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + if (size_max != -1) + len = size_t(size_max); +#endif + std::vector buf(len); + struct passwd pwd, *result; + if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0) + return result->pw_dir; +#endif + return "/"; +} + +std::string path::separator() +{ +#if _WIN32 + return "\\"; +#else + return "/"; +#endif +} + + // executor implementation std::string internal::executor::result(int *exit_code /* = nullptr */) @@ -333,8 +443,12 @@ bool internal::executor::kill() auto previous_windows = m_windows; EnumWindows(&enum_windows_callback, (LPARAM)this); for (auto hwnd : m_windows) - if (previous_windows.find(hwnd) == previous_windows.end()) + if (previous_windows.find(hwnd) == previous_windows.end()) + { SendMessage(hwnd, WM_CLOSE, 0, 0); + // Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox + SendMessage(hwnd, WM_COMMAND, IDNO, 0); + } } #elif __EMSCRIPTEN__ || __NX__ // FIXME: do something @@ -579,7 +693,7 @@ HANDLE internal::platform::new_style_context::create() // crash with error “default context is already set”. sizeof(act_ctx), ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, - "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, nullptr, 0, }; return ::CreateActCtxA(&act_ctx); @@ -850,6 +964,13 @@ internal::file_dialog::file_dialog(type in_type, return ""; }); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)in_type; + (void)title; + (void)default_path; + (void)filters; + (void)options; #else auto command = desktop_helper(); @@ -872,7 +993,14 @@ internal::file_dialog::file_dialog(type in_type, } if (default_path.size()) - script += " default location " + osascript_quote(default_path); + { + if (in_type == type::folder || is_directory(default_path)) + script += " default location "; + else + script += " default name "; + script += osascript_quote(default_path); + } + script += " with prompt " + osascript_quote(title); if (in_type == type::open) @@ -896,11 +1024,17 @@ internal::file_dialog::file_dialog(type in_type, if (pat == "*" || pat == "*.*") has_filter = false; else if (internal::starts_with(pat, "*.")) - filter_list += (filter_list.size() == 0 ? "" : ",") + - osascript_quote(pat.substr(2, pat.size() - 2)); + filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2)); } if (has_filter && filter_list.size() > 0) - script += " of type {" + filter_list + "}"; + { + // There is a weird AppleScript bug where file extensions of length != 3 are + // ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if + // the whole list starts with a 3-character extension, everything works again. + // We use "///" for such an extension because we are sure it cannot appear in + // an actual filename. + script += " of type {\"///\"" + filter_list + "}"; + } } if (in_type == type::open && (options & opt::multiselect)) @@ -922,7 +1056,14 @@ internal::file_dialog::file_dialog(type in_type, else if (is_zenity()) { command.push_back("--file-selection"); - command.push_back("--filename=" + default_path); + + // If the default path is a directory, make sure it ends with "/" otherwise zenity will + // open the file dialog in the parent directory. + auto filename_arg = "--filename=" + default_path; + if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path)) + filename_arg += "/"; + command.push_back(filename_arg); + command.push_back("--title"); command.push_back(title); command.push_back("--separator=\n"); @@ -1062,7 +1203,7 @@ std::string internal::file_dialog::Impl::select_folder_vista(IFileDialog *ifd, b } // Set the dialog title and option to select folders - ifd->SetOptions(FOS_PICKFOLDERS); + ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); ifd->SetTitle(m_wtitle.c_str()); hr = ifd->Show(GetActiveWindow()); @@ -1072,15 +1213,27 @@ std::string internal::file_dialog::Impl::select_folder_vista(IFileDialog *ifd, b hr = ifd->GetResult(&item); if (SUCCEEDED(hr)) { - wchar_t* wselected = nullptr; - item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); - item->Release(); - - if (wselected) + wchar_t* wname = nullptr; + // This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try + // to output a debug message just in case. + if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname))) { - result = internal::wstr2str(std::wstring(wselected)); - internal::platform::dll::proc(internal::platform::ole32_dll(), "CoTaskMemFree")(wselected); + result = internal::wstr2str(std::wstring(wname)); + internal::platform::dll::proc(internal::platform::ole32_dll(), "CoTaskMemFree")(wname); } + else + { + if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) + { + auto name = internal::wstr2str(std::wstring(wname)); + internal::platform::dll::proc(internal::platform::ole32_dll(), "CoTaskMemFree")(wname); + fputs("pfd: failed to get path\n", stderr); + } + else + fputs("pfd: item of unknown type selected\n", stderr); + } + + item->Release(); } } @@ -1161,6 +1314,10 @@ notify::notify(std::string const &title, // Display the new icon Shell_NotifyIconW(NIM_ADD, nid.get()); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)title; + (void)message; #else auto command = desktop_helper(); @@ -1274,45 +1431,45 @@ message::message(std::string const &title, { std::string script = "display dialog " + osascript_quote(text) + " with title " + osascript_quote(title); + auto if_cancel = button::cancel; switch (_choice) { case choice::ok_cancel: script += "buttons {\"OK\", \"Cancel\"}" " default button \"OK\"" " cancel button \"Cancel\""; - m_mappings[256] = button::cancel; break; case choice::yes_no: script += "buttons {\"Yes\", \"No\"}" " default button \"Yes\"" " cancel button \"No\""; - m_mappings[256] = button::no; + if_cancel = button::no; break; case choice::yes_no_cancel: script += "buttons {\"Yes\", \"No\", \"Cancel\"}" " default button \"Yes\"" " cancel button \"Cancel\""; - m_mappings[256] = button::cancel; break; case choice::retry_cancel: script += "buttons {\"Retry\", \"Cancel\"}" " default button \"Retry\"" " cancel button \"Cancel\""; - m_mappings[256] = button::cancel; break; case choice::abort_retry_ignore: script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" - " default button \"Retry\"" + " default button \"Abort\"" " cancel button \"Retry\""; - m_mappings[256] = button::cancel; + if_cancel = button::retry; break; case choice::ok: default: script += "buttons {\"OK\"}" " default button \"OK\"" " cancel button \"OK\""; - m_mappings[256] = button::ok; + if_cancel = button::ok; break; } + m_mappings[1] = if_cancel; + m_mappings[256] = if_cancel; script += " with icon "; switch (_icon) { @@ -1356,6 +1513,7 @@ message::message(std::string const &title, command.insert(command.end(), { "--title", title, "--width=300", "--height=0", // sensible defaults + "--no-markup", // do not interpret text as Pango markup "--text", text, "--icon-name=dialog-" + get_icon_name(_icon) }); } @@ -1411,8 +1569,7 @@ button message::result() auto ret = m_async->result(&exit_code); // osascript will say "button returned:Cancel\n" // and others will just say "Cancel\n" - if (exit_code < 0 || // this means cancel - internal::ends_with(ret, "Cancel\n")) + if (internal::ends_with(ret, "Cancel\n")) return button::cancel; if (internal::ends_with(ret, "OK\n")) return button::ok;