// // Portable File Dialogs // // 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 // and/or modify it under the terms of the Do What the Fuck You Want // to Public License, Version 2, as published by the WTFPL Task Force. // See http://www.wtfpl.net/ for more details. // #pragma once #if _WIN32 #ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN 1 #endif #include #include #include #include // IFileDialog #include #include #include // std::async #include // GetUserProfileDirectory() #elif __EMSCRIPTEN__ #include #else #ifndef _POSIX_C_SOURCE # define _POSIX_C_SOURCE 2 // for popen() #endif #ifdef __APPLE__ # ifndef _DARWIN_C_SOURCE # define _DARWIN_C_SOURCE # endif #endif #include // popen() #include // std::getenv() #include // fcntl() #include // read(), pipe(), dup2(), getuid() #include // ::kill, std::signal #include // stat() #include // waitpid() #include // getpwnam() #endif #include // std::string #include // std::shared_ptr #include // std::ostream #include // std::map #include // std::set #include // std::regex #include // std::mutex, std::this_thread #include // std::chrono // Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog #ifndef PFD_HAS_IFILEDIALOG # define PFD_HAS_IFILEDIALOG 1 # if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION # if __GXX_ABI_VERSION <= 1013 # undef PFD_HAS_IFILEDIALOG # define PFD_HAS_IFILEDIALOG 0 # endif # endif #endif namespace pfd { enum class button { cancel = -1, ok, yes, no, abort, retry, ignore, }; enum class choice { ok = 0, ok_cancel, yes_no, yes_no_cancel, retry_cancel, abort_retry_ignore, }; enum class icon { info = 0, warning, error, question, }; // Additional option flags for various dialog constructors enum class opt : uint8_t { none = 0, // For file open, allow multiselect. multiselect = 0x1, // For file save, force overwrite and disable the confirmation dialog. force_overwrite = 0x2, // For folder select, force path to be the provided argument instead // of the last opened directory, which is the Microsoft-recommended, // user-friendly behaviour. force_path = 0x4, }; inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } // The settings class, only exposing to the user a way to set verbose mode // and to force a rescan of installed desktop helpers (zenity, kdialog…). class settings { public: static bool available(); static void verbose(bool value); static void rescan(); protected: explicit settings(bool resync = false); bool check_program(std::string const &program); inline bool is_osascript() const; inline bool is_zenity() const; inline bool is_kdialog() const; enum class flag { is_scanned = 0, is_verbose, has_zenity, has_matedialog, has_qarma, has_kdialog, is_vista, max_flag, }; // Static array of flags for internal state bool const &flags(flag in_flag) const; // Non-const getter for the static array of flags bool &flags(flag in_flag); }; // Internal classes, not to be used by client applications namespace internal { // Process wait timeout, in milliseconds static int const default_wait_timeout = 20; class executor { friend class dialog; public: // High level function to get the result of a command std::string result(int *exit_code = nullptr); // High level function to abort bool kill(); #if _WIN32 void start_func(std::function const &fun); static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); #elif __EMSCRIPTEN__ void start(int exit_code); #else void start_process(std::vector const &command); #endif ~executor(); protected: bool ready(int timeout = default_wait_timeout); void stop(); private: bool m_running = false; std::string m_stdout; int m_exit_code = -1; #if _WIN32 std::future m_future; std::set m_windows; std::condition_variable m_cond; std::mutex m_mutex; DWORD m_tid; #elif __EMSCRIPTEN__ || __NX__ // FIXME: do something #else pid_t m_pid = 0; int m_fd = -1; #endif }; class platform { protected: #if _WIN32 // Helper class around LoadLibraryA() and GetProcAddress() with some safety class dll { public: dll(std::string const &name); ~dll(); template class proc { public: proc(dll const &lib, std::string const &sym) : m_proc(reinterpret_cast((void *)::GetProcAddress(lib.handle, sym.c_str()))) {} operator bool() const { return m_proc != nullptr; } operator T *() const { return m_proc; } private: T *m_proc; }; private: HMODULE handle; }; // Helper class around CoInitialize() and CoUnInitialize() class ole32_dll : public dll { public: ole32_dll(); ~ole32_dll(); bool is_initialized(); private: HRESULT m_state; }; // Helper class around CreateActCtx() and ActivateActCtx() class new_style_context { public: new_style_context(); ~new_style_context(); private: HANDLE create(); ULONG_PTR m_cookie = 0; }; #endif }; class dialog : protected settings, protected platform { public: bool ready(int timeout = default_wait_timeout) const; bool kill() const; protected: explicit dialog(); std::vector desktop_helper() const; static std::string buttons_to_name(choice _choice); static std::string get_icon_name(icon _icon); std::string powershell_quote(std::string const &str) const; std::string osascript_quote(std::string const &str) const; std::string shell_quote(std::string const &str) const; // Keep handle to executing command std::shared_ptr m_async; }; class file_dialog : public dialog { protected: enum type { open, save, folder, }; file_dialog(type in_type, std::string const &title, std::string const &default_path = "", std::vector const &filters = {}, opt options = opt::none); protected: std::string string_result(); std::vector vector_result(); #if _WIN32 static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); #if PFD_HAS_IFILEDIALOG std::string select_folder_vista(IFileDialog *ifd, bool force_path); #endif std::wstring m_wtitle; std::wstring m_wdefault_path; std::vector m_vector_result; #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(); }; // // The notify widget // class notify : public internal::dialog { public: notify(std::string const &title, std::string const &message, icon _icon = icon::info); }; // // The message widget // class message : public internal::dialog { public: message(std::string const &title, std::string const &text, choice _choice = choice::ok_cancel, icon _icon = icon::info); button result(); private: // Some extra logic to map the exit code to button number std::map m_mappings; }; // // The open_file, save_file, and open_folder widgets // class open_file : public internal::file_dialog { public: open_file(std::string const &title, std::string const &default_path = "", std::vector const &filters = { "All Files", "*" }, opt options = opt::none); #if defined(__has_cpp_attribute) #if __has_cpp_attribute(deprecated) // Backwards compatibility [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] #endif #endif open_file(std::string const &title, std::string const &default_path, std::vector const &filters, bool allow_multiselect); std::vector result(); }; class save_file : public internal::file_dialog { public: save_file(std::string const &title, std::string const &default_path = "", std::vector const &filters = { "All Files", "*" }, opt options = opt::none); #if defined(__has_cpp_attribute) #if __has_cpp_attribute(deprecated) // Backwards compatibility [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] #endif #endif save_file(std::string const &title, std::string const &default_path, std::vector const &filters, bool confirm_overwrite); std::string result(); }; class select_folder : public internal::file_dialog { public: select_folder(std::string const &title, std::string const &default_path = "", opt options = opt::none); std::string result(); }; // // Below this are all the method implementations. You may choose to define the // macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except // in one place. This may reduce compilation times. // #if !defined PFD_SKIP_IMPLEMENTATION // internal free functions implementations namespace internal { #if _WIN32 static inline std::wstring str2wstr(std::string const &str) { int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); std::wstring ret(len, '\0'); MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); return ret; } static inline std::string wstr2str(std::wstring const &str) { int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); std::string ret(len, '\0'); WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); return ret; } static inline bool is_vista() { OSVERSIONINFOEXW osvi; memset(&osvi, 0, sizeof(osvi)); DWORDLONG const mask = VerSetConditionMask( VerSetConditionMask( VerSetConditionMask( 0, VER_MAJORVERSION, VER_GREATER_EQUAL), VER_MINORVERSION, VER_GREATER_EQUAL), VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); osvi.dwOSVersionInfoSize = sizeof(osvi); osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); osvi.wServicePackMajor = 0; return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; } #endif // This is necessary until C++20 which will have std::string::ends_with() etc. static inline bool ends_with(std::string const &str, std::string const &suffix) { return suffix.size() <= str.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } static inline bool starts_with(std::string const &str, std::string const &prefix) { return prefix.size() <= str.size() && 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 _MSC_VER 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 // settings implementation inline settings::settings(bool resync) { flags(flag::is_scanned) &= !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(); #elif !__APPLE__ flags(flag::has_zenity) = check_program("zenity"); flags(flag::has_matedialog) = check_program("matedialog"); flags(flag::has_qarma) = check_program("qarma"); flags(flag::has_kdialog) = check_program("kdialog"); // If multiple helpers are available, try to default to the best one if (flags(flag::has_zenity) && flags(flag::has_kdialog)) { auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP"); if (desktop_name == std::string("gnome")) flags(flag::has_kdialog) = false; else if (desktop_name == std::string("KDE")) flags(flag::has_zenity) = false; } #endif flags(flag::is_scanned) = true; } inline bool settings::available() { #if _WIN32 return true; #elif __APPLE__ return true; #elif __EMSCRIPTEN__ // FIXME: Return true after implementation is complete. return false; #else settings tmp; return tmp.flags(flag::has_zenity) || tmp.flags(flag::has_matedialog) || tmp.flags(flag::has_qarma) || tmp.flags(flag::has_kdialog); #endif } inline void settings::verbose(bool value) { settings().flags(flag::is_verbose) = value; } inline void settings::rescan() { settings(/* resync = */ true); } // Check whether a program is present using “which”. inline bool settings::check_program(std::string const &program) { #if _WIN32 (void)program; return false; #elif __EMSCRIPTEN__ (void)program; return false; #else int exit_code = -1; internal::executor async; async.start_process({"/bin/sh", "-c", "which " + program}); async.result(&exit_code); return exit_code == 0; #endif } inline bool settings::is_osascript() const { #if __APPLE__ return true; #else return false; #endif } inline bool settings::is_zenity() const { return flags(flag::has_zenity) || flags(flag::has_matedialog) || flags(flag::has_qarma); } inline bool settings::is_kdialog() const { return flags(flag::has_kdialog); } inline bool const &settings::flags(flag in_flag) const { static bool flags[size_t(flag::max_flag)]; return flags[size_t(in_flag)]; } inline bool &settings::flags(flag in_flag) { return const_cast(static_cast(this)->flags(in_flag)); } // path implementation inline 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 "/"; } inline std::string path::separator() { #if _WIN32 return "\\"; #else return "/"; #endif } // executor implementation inline std::string internal::executor::result(int *exit_code /* = nullptr */) { stop(); if (exit_code) *exit_code = m_exit_code; return m_stdout; } inline bool internal::executor::kill() { #if _WIN32 if (m_future.valid()) { // Close all windows that weren’t open when we started the future auto previous_windows = m_windows; EnumWindows(&enum_windows_callback, (LPARAM)this); for (auto hwnd : m_windows) 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 return false; // cannot kill #else ::kill(m_pid, SIGKILL); #endif stop(); return true; } #if _WIN32 inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) { auto that = (executor *)lParam; DWORD pid; auto tid = GetWindowThreadProcessId(hwnd, &pid); if (tid == that->m_tid) that->m_windows.insert(hwnd); return TRUE; } #endif #if _WIN32 inline void internal::executor::start_func(std::function const &fun) { stop(); auto trampoline = [fun, this]() { // Save our thread id so that the caller can cancel us m_tid = GetCurrentThreadId(); EnumWindows(&enum_windows_callback, (LPARAM)this); m_cond.notify_all(); return fun(&m_exit_code); }; std::unique_lock lock(m_mutex); m_future = std::async(std::launch::async, trampoline); m_cond.wait(lock); m_running = true; } #elif __EMSCRIPTEN__ inline void internal::executor::start(int exit_code) { m_exit_code = exit_code; } #else inline void internal::executor::start_process(std::vector const &command) { stop(); m_stdout.clear(); m_exit_code = -1; int in[2], out[2]; if (pipe(in) != 0 || pipe(out) != 0) return; m_pid = fork(); if (m_pid < 0) return; close(in[m_pid ? 0 : 1]); close(out[m_pid ? 1 : 0]); if (m_pid == 0) { dup2(in[0], STDIN_FILENO); dup2(out[1], STDOUT_FILENO); // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) int fd = open("/dev/null", O_WRONLY); dup2(fd, STDERR_FILENO); close(fd); std::vector args; std::transform(command.cbegin(), command.cend(), std::back_inserter(args), [](std::string const &s) { return const_cast(s.c_str()); }); args.push_back(nullptr); // null-terminate argv[] execvp(args[0], args.data()); exit(1); } close(in[1]); m_fd = out[0]; auto flags = fcntl(m_fd, F_GETFL); fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); m_running = true; } #endif inline internal::executor::~executor() { stop(); } inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) { if (!m_running) return true; #if _WIN32 if (m_future.valid()) { auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); if (status != std::future_status::ready) { // On Windows, we need to run the message pump. If the async // thread uses a Windows API dialog, it may be attached to the // main thread and waiting for messages that only we can dispatch. MSG msg; while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } return false; } m_stdout = m_future.get(); } #elif __EMSCRIPTEN__ || __NX__ // FIXME: do something (void)timeout; #else char buf[BUFSIZ]; ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore if (received > 0) { m_stdout += std::string(buf, received); return false; } // Reap child process if it is dead. It is possible that the system has already reaped it // (this happens when the calling application handles or ignores SIG_CHLD) and results in // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for // a little while. int status; pid_t child = waitpid(m_pid, &status, WNOHANG); if (child != m_pid && (child >= 0 || errno != ECHILD)) { // FIXME: this happens almost always at first iteration std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); return false; } close(m_fd); m_exit_code = WEXITSTATUS(status); #endif m_running = false; return true; } inline void internal::executor::stop() { // Loop until the user closes the dialog while (!ready()) ; } // dll implementation #if _WIN32 inline internal::platform::dll::dll(std::string const &name) : handle(::LoadLibraryA(name.c_str())) {} inline internal::platform::dll::~dll() { if (handle) ::FreeLibrary(handle); } #endif // _WIN32 // ole32_dll implementation #if _WIN32 inline internal::platform::ole32_dll::ole32_dll() : dll("ole32.dll") { // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. // See https://github.com/samhocevar/portable-file-dialogs/issues/51 auto coinit = proc(*this, "CoInitializeEx"); m_state = coinit(nullptr, COINIT_MULTITHREADED); } inline internal::platform::ole32_dll::~ole32_dll() { if (is_initialized()) proc(*this, "CoUninitialize")(); } inline bool internal::platform::ole32_dll::is_initialized() { return m_state == S_OK || m_state == S_FALSE; } #endif // new_style_context implementation #if _WIN32 inline internal::platform::new_style_context::new_style_context() { // Only create one activation context for the whole app lifetime. static HANDLE hctx = create(); if (hctx != INVALID_HANDLE_VALUE) ActivateActCtx(hctx, &m_cookie); } inline internal::platform::new_style_context::~new_style_context() { DeactivateActCtx(0, m_cookie); } inline HANDLE internal::platform::new_style_context::create() { // This “hack” seems to be necessary for this code to work on windows XP. // Without it, dialogs do not show and close immediately. GetError() // returns 0 so I don’t know what causes this. I was not able to reproduce // this behavior on Windows 7 and 10 but just in case, let it be here for // those versions too. // This hack is not required if other dialogs are used (they load comdlg32 // automatically), only if message boxes are used. dll comdlg32("comdlg32.dll"); // Using approach as shown here: https://stackoverflow.com/a/10444161 UINT len = ::GetSystemDirectoryA(nullptr, 0); std::string sys_dir(len, '\0'); ::GetSystemDirectoryA(&sys_dir[0], len); ACTCTXA act_ctx = { // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a // 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, nullptr, 0, }; return ::CreateActCtxA(&act_ctx); } #endif // _WIN32 // dialog implementation inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const { return m_async->ready(timeout); } inline bool internal::dialog::kill() const { return m_async->kill(); } inline internal::dialog::dialog() : m_async(std::make_shared()) { } inline std::vector internal::dialog::desktop_helper() const { #if __APPLE__ return { "osascript" }; #else return { flags(flag::has_zenity) ? "zenity" : flags(flag::has_matedialog) ? "matedialog" : flags(flag::has_qarma) ? "qarma" : flags(flag::has_kdialog) ? "kdialog" : "echo" }; #endif } inline std::string internal::dialog::buttons_to_name(choice _choice) { switch (_choice) { case choice::ok_cancel: return "okcancel"; case choice::yes_no: return "yesno"; case choice::yes_no_cancel: return "yesnocancel"; case choice::retry_cancel: return "retrycancel"; case choice::abort_retry_ignore: return "abortretryignore"; /* case choice::ok: */ default: return "ok"; } } inline std::string internal::dialog::get_icon_name(icon _icon) { switch (_icon) { case icon::warning: return "warning"; case icon::error: return "error"; case icon::question: return "question"; // Zenity wants "information" but WinForms wants "info" /* case icon::info: */ default: #if _WIN32 return "info"; #else return "information"; #endif } } // This is only used for debugging purposes inline std::ostream& operator <<(std::ostream &s, std::vector const &v) { int not_first = 0; for (auto &e : v) s << (not_first++ ? " " : "") << e; return s; } // Properly quote a string for Powershell: replace ' or " with '' or "" // FIXME: we should probably get rid of newlines! // FIXME: the \" sequence seems unsafe, too! // XXX: this is no longer used but I would like to keep it around just in case inline std::string internal::dialog::powershell_quote(std::string const &str) const { return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; } // Properly quote a string for osascript: replace \ or " with \\ or \" // XXX: this also used to replace ' with \' when popen was used, but it would be // smarter to do shell_quote(osascript_quote(...)) if this is needed again. inline std::string internal::dialog::osascript_quote(std::string const &str) const { return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; } // Properly quote a string for the shell: just replace ' with '\'' // XXX: this is no longer used but I would like to keep it around just in case inline std::string internal::dialog::shell_quote(std::string const &str) const { return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; } // file_dialog implementation inline internal::file_dialog::file_dialog(type in_type, std::string const &title, std::string const &default_path /* = "" */, std::vector const &filters /* = {} */, opt options /* = opt::none */) { #if _WIN32 std::string filter_list; std::regex whitespace(" *"); for (size_t i = 0; i + 1 < filters.size(); i += 2) { filter_list += filters[i] + '\0'; filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; } filter_list += '\0'; m_async->start_func([this, in_type, title, default_path, filter_list, options](int *exit_code) -> std::string { (void)exit_code; m_wtitle = internal::str2wstr(title); m_wdefault_path = internal::str2wstr(default_path); auto wfilter_list = internal::str2wstr(filter_list); // Initialise COM. This is required for the new folder selection window, // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) // and to avoid random crashes with GetOpenFileNameW() (see // https://github.com/samhocevar/portable-file-dialogs/issues/51) ole32_dll ole32; // Folder selection uses a different method if (in_type == type::folder) { #if PFD_HAS_IFILEDIALOG if (flags(flag::is_vista)) { // On Vista and higher we should be able to use IFileDialog for folder selection IFileDialog *ifd; HRESULT hr = dll::proc(ole32, "CoCreateInstance") (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); // In case CoCreateInstance fails (which it should not), try legacy approach if (SUCCEEDED(hr)) return select_folder_vista(ifd, options & opt::force_path); } #endif BROWSEINFOW bi; memset(&bi, 0, sizeof(bi)); bi.lpfn = &bffcallback; bi.lParam = (LPARAM)this; if (flags(flag::is_vista)) { if (ole32.is_initialized()) bi.ulFlags |= BIF_NEWDIALOGSTYLE; bi.ulFlags |= BIF_EDITBOX; bi.ulFlags |= BIF_STATUSTEXT; } auto *list = SHBrowseForFolderW(&bi); std::string ret; if (list) { auto buffer = new wchar_t[MAX_PATH]; SHGetPathFromIDListW(list, buffer); dll::proc(ole32, "CoTaskMemFree")(list); ret = internal::wstr2str(buffer); delete[] buffer; } return ret; } OPENFILENAMEW ofn; memset(&ofn, 0, sizeof(ofn)); ofn.lStructSize = sizeof(OPENFILENAMEW); ofn.hwndOwner = GetActiveWindow(); ofn.lpstrFilter = wfilter_list.c_str(); auto woutput = std::wstring(MAX_PATH * 256, L'\0'); ofn.lpstrFile = (LPWSTR)woutput.data(); ofn.nMaxFile = (DWORD)woutput.size(); if (!m_wdefault_path.empty()) { // If a directory was provided, use it as the initial directory. If // a valid path was provided, use it as the initial file. Otherwise, // let the Windows API decide. auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) ofn.lpstrInitialDir = m_wdefault_path.c_str(); else if (m_wdefault_path.size() <= woutput.size()) //second argument is size of buffer, not length of string StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); else { ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); } } ofn.lpstrTitle = m_wtitle.c_str(); ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; dll comdlg32("comdlg32.dll"); // Apply new visual style (required for windows XP) new_style_context ctx; if (in_type == type::save) { if (!(options & opt::force_overwrite)) ofn.Flags |= OFN_OVERWRITEPROMPT; dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); if (get_save_file_name(&ofn) == 0) return ""; return internal::wstr2str(woutput.c_str()); } else { if (options & opt::multiselect) ofn.Flags |= OFN_ALLOWMULTISELECT; ofn.Flags |= OFN_PATHMUSTEXIST; dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); if (get_open_file_name(&ofn) == 0) return ""; } std::string prefix; for (wchar_t const *p = woutput.c_str(); *p; ) { auto filename = internal::wstr2str(p); p += wcslen(p); // In multiselect mode, we advance p one wchar further and // check for another filename. If there is one and the // prefix is empty, it means we just read the prefix. if ((options & opt::multiselect) && *++p && prefix.empty()) { prefix = filename + "/"; continue; } m_vector_result.push_back(prefix + filename); } return ""; }); #elif __EMSCRIPTEN__ // FIXME: do something (void)in_type; (void)title; (void)default_path; (void)filters; (void)options; #else auto command = desktop_helper(); if (is_osascript()) { std::string script = "set ret to choose"; switch (in_type) { case type::save: script += " file name"; break; case type::open: default: script += " file"; if (options & opt::multiselect) script += " with multiple selections allowed"; break; case type::folder: script += " folder"; break; } if (default_path.size()) { 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) { // Concatenate all user-provided filter patterns std::string patterns; for (size_t i = 0; i < filters.size() / 2; ++i) patterns += " " + filters[2 * i + 1]; // Split the pattern list to check whether "*" is in there; if it // is, we have to disable filters because there is no mechanism in // OS X for the user to override the filter. std::regex sep("\\s+"); std::string filter_list; bool has_filter = true; std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); std::sregex_token_iterator end; for ( ; iter != end; ++iter) { auto pat = iter->str(); if (pat == "*" || pat == "*.*") has_filter = false; else if (internal::starts_with(pat, "*.")) filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2)); } if (has_filter && filter_list.size() > 0) { // 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)) { script += "\nset s to \"\""; script += "\nrepeat with i in ret"; script += "\n set s to s & (POSIX path of i) & \"\\n\""; script += "\nend repeat"; script += "\ncopy s to stdout"; } else { script += "\nPOSIX path of ret"; } command.push_back("-e"); command.push_back(script); } else if (is_zenity()) { command.push_back("--file-selection"); // 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"); for (size_t i = 0; i < filters.size() / 2; ++i) { command.push_back("--file-filter"); command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); } if (in_type == type::save) command.push_back("--save"); if (in_type == type::folder) command.push_back("--directory"); if (!(options & opt::force_overwrite)) command.push_back("--confirm-overwrite"); if (options & opt::multiselect) command.push_back("--multiple"); } else if (is_kdialog()) { switch (in_type) { case type::save: command.push_back("--getsavefilename"); break; case type::open: command.push_back("--getopenfilename"); break; case type::folder: command.push_back("--getexistingdirectory"); break; } if (options & opt::multiselect) { command.push_back("--multiple"); command.push_back("--separate-output"); } command.push_back(default_path); std::string filter; for (size_t i = 0; i < filters.size() / 2; ++i) filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; command.push_back(filter); command.push_back("--title"); command.push_back(title); } if (flags(flag::is_verbose)) std::cerr << "pfd: " << command << std::endl; m_async->start_process(command); #endif } inline std::string internal::file_dialog::string_result() { #if _WIN32 return m_async->result(); #else auto ret = m_async->result(); // Strip potential trailing newline (zenity). Also strip trailing slash // added by osascript for consistency with other backends. while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) ret.pop_back(); return ret; #endif } inline std::vector internal::file_dialog::vector_result() { #if _WIN32 m_async->result(); return m_vector_result; #else std::vector ret; auto result = m_async->result(); for (;;) { // Split result along newline characters auto i = result.find('\n'); if (i == 0 || i == std::string::npos) break; ret.push_back(result.substr(0, i)); result = result.substr(i + 1, result.size()); } return ret; #endif } #if _WIN32 // Use a static function to pass as BFFCALLBACK for legacy folder select inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData) { auto inst = (file_dialog *)pData; switch (uMsg) { case BFFM_INITIALIZED: SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); break; } return 0; } #if PFD_HAS_IFILEDIALOG inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) { std::string result; IShellItem *folder; // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) dll shell32("shell32.dll"); dll::proc create_item(shell32, "SHCreateItemFromParsingName"); if (!create_item) return ""; auto hr = create_item(m_wdefault_path.c_str(), nullptr, IID_PPV_ARGS(&folder)); // Set default folder if found. This only sets the default folder. If // Windows has any info about the most recently selected folder, it // will display it instead. Generally, calling SetFolder() to set the // current directory “is not a good or expected user experience and // should therefore be avoided”: // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder if (SUCCEEDED(hr)) { if (force_path) ifd->SetFolder(folder); else ifd->SetDefaultFolder(folder); folder->Release(); } // Set the dialog title and option to select folders ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); ifd->SetTitle(m_wtitle.c_str()); hr = ifd->Show(GetActiveWindow()); if (SUCCEEDED(hr)) { IShellItem* item; hr = ifd->GetResult(&item); if (SUCCEEDED(hr)) { 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(wname)); dll::proc(ole32_dll(), "CoTaskMemFree")(wname); } else { if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) { auto name = internal::wstr2str(std::wstring(wname)); dll::proc(ole32_dll(), "CoTaskMemFree")(wname); std::cerr << "pfd: failed to get path for " << name << std::endl; } else std::cerr << "pfd: item of unknown type selected" << std::endl; } item->Release(); } } ifd->Release(); return result; } #endif #endif // notify implementation inline notify::notify(std::string const &title, std::string const &message, icon _icon /* = icon::info */) { if (_icon == icon::question) // Not supported by notifications _icon = icon::info; #if _WIN32 // Use a static shared pointer for notify_icon so that we can delete // it whenever we need to display a new one, and we can also wait // until the program has finished running. struct notify_icon_data : public NOTIFYICONDATAW { ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } }; static std::shared_ptr nid; // Release the previous notification icon, if any, and allocate a new // one. Note that std::make_shared() does value initialization, so there // is no need to memset the structure. nid = nullptr; nid = std::make_shared(); // For XP support nid->cbSize = NOTIFYICONDATAW_V2_SIZE; nid->hWnd = nullptr; nid->uID = 0; // Flag Description: // - NIF_ICON The hIcon member is valid. // - NIF_MESSAGE The uCallbackMessage member is valid. // - NIF_TIP The szTip member is valid. // - NIF_STATE The dwState and dwStateMask members are valid. // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. // - NIF_GUID Reserved. nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; // Flag Description // - NIIF_ERROR An error icon. // - NIIF_INFO An information icon. // - NIIF_NONE No icon. // - NIIF_WARNING A warning icon. // - NIIF_ICON_MASK Version 6.0. Reserved. // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips switch (_icon) { case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; } ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL { ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); return false; }; nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); nid->uTimeout = 5000; StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); // Display the new icon Shell_NotifyIconW(NIM_ADD, nid.get()); #elif __EMSCRIPTEN__ // FIXME: do something (void)title; (void)message; #else auto command = desktop_helper(); if (is_osascript()) { command.push_back("-e"); command.push_back("display notification " + osascript_quote(message) + " with title " + osascript_quote(title)); } else if (is_zenity()) { command.push_back("--notification"); command.push_back("--window-icon"); command.push_back(get_icon_name(_icon)); command.push_back("--text"); command.push_back(title + "\n" + message); } else if (is_kdialog()) { command.push_back("--icon"); command.push_back(get_icon_name(_icon)); command.push_back("--title"); command.push_back(title); command.push_back("--passivepopup"); command.push_back(message); command.push_back("5"); } if (flags(flag::is_verbose)) std::cerr << "pfd: " << command << std::endl; m_async->start_process(command); #endif } // message implementation inline message::message(std::string const &title, std::string const &text, choice _choice /* = choice::ok_cancel */, icon _icon /* = icon::info */) { #if _WIN32 // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 UINT style = MB_SYSTEMMODAL; switch (_icon) { case icon::warning: style |= MB_ICONWARNING; break; case icon::error: style |= MB_ICONERROR; break; case icon::question: style |= MB_ICONQUESTION; break; /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; } switch (_choice) { case choice::ok_cancel: style |= MB_OKCANCEL; break; case choice::yes_no: style |= MB_YESNO; break; case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; case choice::retry_cancel: style |= MB_RETRYCANCEL; break; case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; /* case choice::ok: */ default: style |= MB_OK; break; } m_mappings[IDCANCEL] = button::cancel; m_mappings[IDOK] = button::ok; m_mappings[IDYES] = button::yes; m_mappings[IDNO] = button::no; m_mappings[IDABORT] = button::abort; m_mappings[IDRETRY] = button::retry; m_mappings[IDIGNORE] = button::ignore; m_async->start_func([text, title, style](int* exit_code) -> std::string { auto wtext = internal::str2wstr(text); auto wtitle = internal::str2wstr(title); // Apply new visual style (required for all Windows versions) new_style_context ctx; *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); return ""; }); #elif __EMSCRIPTEN__ std::string full_message; switch (_icon) { case icon::warning: full_message = "⚠️"; break; case icon::error: full_message = "⛔"; break; case icon::question: full_message = "❓"; break; /* case icon::info: */ default: full_message = "ℹ"; break; } full_message += ' ' + title + "\n\n" + text; // This does not really start an async task; it just passes the // EM_ASM_INT return value to a fake start() function. m_async->start(EM_ASM_INT( { if ($1) return window.confirm(UTF8ToString($0)) ? 0 : -1; alert(UTF8ToString($0)); return 0; }, full_message.c_str(), _choice == choice::ok_cancel)); #else auto command = desktop_helper(); if (is_osascript()) { 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\""; break; case choice::yes_no: script += "buttons {\"Yes\", \"No\"}" " default button \"Yes\"" " cancel button \"No\""; if_cancel = button::no; break; case choice::yes_no_cancel: script += "buttons {\"Yes\", \"No\", \"Cancel\"}" " default button \"Yes\"" " cancel button \"Cancel\""; break; case choice::retry_cancel: script += "buttons {\"Retry\", \"Cancel\"}" " default button \"Retry\"" " cancel button \"Cancel\""; break; case choice::abort_retry_ignore: script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" " default button \"Abort\"" " cancel button \"Retry\""; if_cancel = button::retry; break; case choice::ok: default: script += "buttons {\"OK\"}" " default button \"OK\"" " cancel button \"OK\""; if_cancel = button::ok; break; } m_mappings[1] = if_cancel; m_mappings[256] = if_cancel; // XXX: I think this was never correct script += " with icon "; switch (_icon) { #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; case icon::warning: script += "caution"; break; case icon::error: script += "stop"; break; case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; #undef PFD_OSX_ICON } command.push_back("-e"); command.push_back(script); } else if (is_zenity()) { switch (_choice) { case choice::ok_cancel: command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; case choice::yes_no: // Do not use standard --question because it causes “No” to return -1, // which is inconsistent with the “Yes/No/Cancel” mode below. command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; case choice::yes_no_cancel: command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; case choice::retry_cancel: command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; case choice::abort_retry_ignore: command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; case choice::ok: default: switch (_icon) { case icon::error: command.push_back("--error"); break; case icon::warning: command.push_back("--warning"); break; default: command.push_back("--info"); break; } } 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) }); } else if (is_kdialog()) { if (_choice == choice::ok) { switch (_icon) { case icon::error: command.push_back("--error"); break; case icon::warning: command.push_back("--sorry"); break; default: command.push_back("--msgbox"); break; } } else { std::string flag = "--"; if (_icon == icon::warning || _icon == icon::error) flag += "warning"; flag += "yesno"; if (_choice == choice::yes_no_cancel) flag += "cancel"; command.push_back(flag); if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) { m_mappings[0] = button::yes; m_mappings[256] = button::no; } } command.push_back(text); command.push_back("--title"); command.push_back(title); // Must be after the above part if (_choice == choice::ok_cancel) command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); } if (flags(flag::is_verbose)) std::cerr << "pfd: " << command << std::endl; m_async->start_process(command); #endif } inline button message::result() { int exit_code; auto ret = m_async->result(&exit_code); // osascript will say "button returned:Cancel\n" // and others will just say "Cancel\n" if (internal::ends_with(ret, "Cancel\n")) return button::cancel; if (internal::ends_with(ret, "OK\n")) return button::ok; if (internal::ends_with(ret, "Yes\n")) return button::yes; if (internal::ends_with(ret, "No\n")) return button::no; if (internal::ends_with(ret, "Abort\n")) return button::abort; if (internal::ends_with(ret, "Retry\n")) return button::retry; if (internal::ends_with(ret, "Ignore\n")) return button::ignore; if (m_mappings.count(exit_code) != 0) return m_mappings[exit_code]; return exit_code == 0 ? button::ok : button::cancel; } // open_file implementation inline open_file::open_file(std::string const &title, std::string const &default_path /* = "" */, std::vector const &filters /* = { "All Files", "*" } */, opt options /* = opt::none */) : file_dialog(type::open, title, default_path, filters, options) { } inline open_file::open_file(std::string const &title, std::string const &default_path, std::vector const &filters, bool allow_multiselect) : open_file(title, default_path, filters, (allow_multiselect ? opt::multiselect : opt::none)) { } inline std::vector open_file::result() { return vector_result(); } // save_file implementation inline save_file::save_file(std::string const &title, std::string const &default_path /* = "" */, std::vector const &filters /* = { "All Files", "*" } */, opt options /* = opt::none */) : file_dialog(type::save, title, default_path, filters, options) { } inline save_file::save_file(std::string const &title, std::string const &default_path, std::vector const &filters, bool confirm_overwrite) : save_file(title, default_path, filters, (confirm_overwrite ? opt::none : opt::force_overwrite)) { } inline std::string save_file::result() { return string_result(); } // select_folder implementation inline select_folder::select_folder(std::string const &title, std::string const &default_path /* = "" */, opt options /* = opt::none */) : file_dialog(type::folder, title, default_path, {}, options) { } inline std::string select_folder::result() { return string_result(); } #endif // PFD_SKIP_IMPLEMENTATION } // namespace pfd