From 1ae8da3b3f4d31d56dda51575af70d860c12e2ac Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 12 Sep 2020 21:15:02 -0700 Subject: [PATCH] [wpigui] Move portable-file-dialogs.h to wpigui and upgrade it (#2704) Also split source and implementation to avoid platform headers in header. --- .styleguide | 1 - .../native/include/portable-file-dialogs.h | 1305 ---------------- wpigui/.styleguide | 5 + .../main/native/cpp/portable-file-dialogs.cpp | 1366 +++++++++++++++++ .../native/include/portable-file-dialogs.h | 249 +++ 5 files changed, 1620 insertions(+), 1306 deletions(-) delete mode 100644 simulation/halsim_gui/src/main/native/include/portable-file-dialogs.h create mode 100644 wpigui/src/main/native/cpp/portable-file-dialogs.cpp create mode 100644 wpigui/src/main/native/include/portable-file-dialogs.h diff --git a/.styleguide b/.styleguide index bec7a2f60f..485ac26947 100644 --- a/.styleguide +++ b/.styleguide @@ -12,7 +12,6 @@ cppSrcFileInclude { generatedFileExclude { FRCNetComm\.java$ simulation/gz_msgs/src/include/simulation/gz_msgs/msgs\.h$ - simulation/halsim_gui/src/main/native/include/portable-file-dialogs\.h$ } repoRootNameOverride { diff --git a/simulation/halsim_gui/src/main/native/include/portable-file-dialogs.h b/simulation/halsim_gui/src/main/native/include/portable-file-dialogs.h deleted file mode 100644 index 1fbd11cf7f..0000000000 --- a/simulation/halsim_gui/src/main/native/include/portable-file-dialogs.h +++ /dev/null @@ -1,1305 +0,0 @@ -// -// Portable File Dialogs -// -// Copyright (c) 2018—2019 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 **** 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 -#include -#else -#ifndef _POSIX_C_SOURCE -# define _POSIX_C_SOURCE 2 // for popen() -#endif -#include // for popen() -#include // for std::getenv() -#include // for fcntl() -#include // for read() -#endif - -#include -#include -#include -#include -#include -#include -#include - -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, -}; - -// Process wait timeout, in milliseconds -static int const default_wait_timeout = 20; - -// 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 void verbose(bool value) - { - settings().flags(flag::is_verbose) = value; - } - - static void rescan() - { - settings(true); - } - -protected: - enum class flag - { - is_scanned = 0, - is_verbose, - - has_zenity, - has_matedialog, - has_qarma, - has_kdialog, - is_vista, - - max_flag, - }; - - explicit settings(bool resync = false) - { - flags(flag::is_scanned) &= !resync; - } - - inline bool is_osascript() const - { -#if __APPLE__ - return true; -#else - return false; -#endif - } - - inline bool is_zenity() const - { - return flags(flag::has_zenity) || - flags(flag::has_matedialog) || - flags(flag::has_qarma); - } - - inline bool is_kdialog() const - { - return flags(flag::has_kdialog); - } - - // Static array of flags for internal state - bool const &flags(flag in_flag) const - { - static bool flags[(size_t)flag::max_flag]; - return flags[(size_t)in_flag]; - } - - // Non-const getter for the static array of flags - bool &flags(flag in_flag) - { - return const_cast(static_cast(this)->flags(in_flag)); - } -}; - -// Forward declarations for our API -class notify; -class message; - -// Internal classes, not to be used by client applications -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; -} - -class executor -{ - friend class dialog; - -public: - // High level function to get the result of a command - std::string result(int *exit_code = nullptr) - { - stop(); - if (exit_code) - *exit_code = m_exit_code; - return m_stdout; - } - -#if _WIN32 - void start(std::function const &fun) - { - stop(); - m_future = std::async(fun, &m_exit_code); - m_running = true; - } -#endif - - void start(std::string const &command) - { - stop(); - m_stdout.clear(); - m_exit_code = -1; - -#if _WIN32 - STARTUPINFOW si; - - memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - - std::wstring wcommand = str2wstr(command); - if (!CreateProcessW(nullptr, (LPWSTR)wcommand.c_str(), nullptr, nullptr, - FALSE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &m_pi)) - return; /* TODO: GetLastError()? */ - WaitForInputIdle(m_pi.hProcess, INFINITE); -#elif __EMSCRIPTEN__ || __NX__ - // FIXME: do something -#else - m_stream = popen((command + " 2>/dev/null").c_str(), "r"); - if (!m_stream) - return; - m_fd = fileno(m_stream); - fcntl(m_fd, F_SETFL, O_NONBLOCK); -#endif - m_running = true; - } - - ~executor() - { - stop(); - } - -protected: - bool 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) - return false; - - m_stdout = m_future.get(); - } - else - { - if (WaitForSingleObject(m_pi.hProcess, timeout) == WAIT_TIMEOUT) - return false; - - DWORD ret; - GetExitCodeProcess(m_pi.hProcess, &ret); - m_exit_code = (int)ret; - CloseHandle(m_pi.hThread); - CloseHandle(m_pi.hProcess); - } -#elif __EMSCRIPTEN__ || __NX__ - // FIXME: do something -#else - char buf[BUFSIZ]; - ssize_t received = read(m_fd, buf, BUFSIZ - 1); - if (received == -1 && errno == EAGAIN) - { - // FIXME: this happens almost always at first iteration - std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); - return false; - } - if (received > 0) - { - m_stdout += std::string(buf, received); - return false; - } - m_exit_code = pclose(m_stream); -#endif - - m_running = false; - return true; - } - - void stop() - { - // Loop until the user closes the dialog - while (!ready()) - { -#if _WIN32 - // 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); - } -#endif - } - } - -private: - bool m_running = false; - std::string m_stdout; - int m_exit_code = -1; -#if _WIN32 - std::future m_future; - PROCESS_INFORMATION m_pi; -#else - FILE *m_stream = nullptr; - 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) - : handle(::LoadLibraryA(name.c_str())) - {} - - ~dll() - { - if (handle) - ::FreeLibrary(handle); - } - - template class proc - { - public: - proc(dll const &lib, std::string const &sym) - : m_proc(reinterpret_cast(::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 CreateActCtx() and ActivateActCtx() - class new_style_context - { - public: - 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); - } - - ~new_style_context() - { - DeactivateActCtx(0, m_cookie); - } - - private: - HANDLE 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, - }; - - return ::CreateActCtxA(&act_ctx); - } - - ULONG_PTR m_cookie = 0; - }; -#endif -}; - -class dialog : protected settings, protected platform -{ - friend class pfd::notify; - friend class pfd::message; - -public: - bool ready(int timeout = default_wait_timeout) - { - return m_async->ready(timeout); - } - -protected: - explicit dialog() - : m_async(std::make_shared()) - { - if (!flags(flag::is_scanned)) - { -#if _WIN32 - flags(flag::is_vista) = 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 = std::getenv("XDG_SESSION_DESKTOP"); - if (desktop_name && desktop_name == std::string("gnome")) - flags(flag::has_kdialog) = false; - else if (desktop_name && desktop_name == std::string("KDE")) - flags(flag::has_zenity) = false; - } -#endif - flags(flag::is_scanned) = true; - } - } - - std::string 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 - } - - std::string buttons_to_name(choice choice) const - { - 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"; - } - } - - std::string get_icon_name(icon icon) const - { - 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 - } - } - - // Properly quote a string for Powershell: replace ' or " with '' or "" - // FIXME: we should probably get rid of newlines! - // FIXME: the \" sequence seems unsafe, too! - std::string powershell_quote(std::string const &str) const - { - return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; - } - - // Properly quote a string for osascript: replace ' with '\'' and \ or " with \\ or \" - std::string osascript_quote(std::string const &str) const - { - return "\"" + std::regex_replace(std::regex_replace(str, - std::regex("[\\\\\"]"), "\\$&"), std::regex("'"), "'\\''") + "\""; - } - - // Properly quote a string for the shell: just replace ' with '\'' - std::string shell_quote(std::string const &str) const - { - return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; - } - - // Check whether a program is present using “which”. - bool check_program(std::string const &program) - { -#if _WIN32 - (void)program; - return false; -#else - int exit_code = -1; - m_async->start("which " + program + " 2>/dev/null"); - m_async->result(&exit_code); - return exit_code == 0; -#endif - } - -protected: - // 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 filters = {}, - bool allow_multiselect = false, - bool confirm_overwrite = true) - { -#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([this, in_type, title, default_path, filter_list, - allow_multiselect, confirm_overwrite](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); - - // Folder selection uses a different method - if (in_type == type::folder) - { - dll ole32("ole32.dll"); - - auto status = dll::proc(ole32, "CoInitializeEx") - (nullptr, COINIT_APARTMENTTHREADED); - 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); - } - - BROWSEINFOW bi; - memset(&bi, 0, sizeof(bi)); - - bi.lpfn = &bffcallback; - bi.lParam = (LPARAM)this; - - if (flags(flag::is_vista)) - { - // This hangs on Windows XP, as reported here: - // https://github.com/samhocevar/portable-file-dialogs/pull/21 - if (status == S_OK) - 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; - } - if (status == S_OK) - dll::proc(ole32, "CoUninitialize")(); - return ret; - } - - OPENFILENAMEW ofn; - memset(&ofn, 0, sizeof(ofn)); - ofn.lStructSize = sizeof(OPENFILENAMEW); - ofn.hwndOwner = GetForegroundWindow(); - - 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()) - lstrcpyW(ofn.lpstrFile, 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"); - - if (in_type == type::save) - { - if (confirm_overwrite) - ofn.Flags |= OFN_OVERWRITEPROMPT; - - // using set context to apply new visual style (required for windows XP) - new_style_context ctx; - - dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); - if (get_save_file_name(&ofn) == 0) - return ""; - return internal::wstr2str(woutput.c_str()); - } - - if (allow_multiselect) - ofn.Flags |= OFN_ALLOWMULTISELECT; - ofn.Flags |= OFN_PATHMUSTEXIST; - - // using set context to apply new visual style (required for windows XP) - new_style_context ctx; - - 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 += filename.size(); - // In multiselect mode, we advance p one step more and - // check for another filename. If there is one and the - // prefix is empty, it means we just read the prefix. - if (allow_multiselect && *++p && prefix.empty()) - { - prefix = filename + "/"; - continue; - } - - m_vector_result.push_back(prefix + filename); - } - - return ""; - }); -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - command += " -e 'set ret to choose"; - switch (in_type) - { - case type::save: - command += " file name"; - break; - case type::open: default: - command += " file"; - if (allow_multiselect) - command += " with multiple selections allowed"; - break; - case type::folder: - command += " folder"; - break; - } - - if (default_path.size()) - command += " default location " + osascript_quote(default_path); - command += " 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 += (filter_list.size() == 0 ? "" : ",") + - osascript_quote(pat.substr(2, pat.size() - 2)); - } - if (has_filter && filter_list.size() > 0) - command += " of type {" + filter_list + "}"; - } - - if (in_type == type::open && allow_multiselect) - { - command += "\nset s to \"\""; - command += "\nrepeat with i in ret"; - command += "\n set s to s & (POSIX path of i) & \"\\n\""; - command += "\nend repeat"; - command += "\ncopy s to stdout'"; - } - else - { - command += "\nPOSIX path of ret'"; - } - } - else if (is_zenity()) - { - command += " --file-selection --filename=" + shell_quote(default_path) - + " --title " + shell_quote(title) - + " --separator='\n'"; - - for (size_t i = 0; i < filters.size() / 2; ++i) - command += " --file-filter " + shell_quote(filters[2 * i] + "|" + filters[2 * i + 1]); - - if (in_type == type::save) - command += " --save"; - if (in_type == type::folder) - command += " --directory"; - if (confirm_overwrite) - command += " --confirm-overwrite"; - if (allow_multiselect) - command += " --multiple"; - } - else if (is_kdialog()) - { - switch (in_type) - { - case type::save: command += " --getsavefilename"; break; - case type::open: command += " --getopenfilename"; break; - case type::folder: command += " --getexistingdirectory"; break; - } - command += " " + shell_quote(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 += " " + shell_quote(filter); - - command += " --title " + shell_quote(title); - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start(command); -#endif - } - -protected: - std::string string_result() - { -#if _WIN32 - return m_async->result(); -#else - // Strip the newline character - auto ret = m_async->result(); - return ret.back() == '\n' ? ret.substr(0, ret.size() - 1) : ret; -#endif - } - - std::vector 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 - static int CALLBACK 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; - } - - std::string select_folder_vista(IFileDialog *ifd) - { - 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)) - { - ifd->SetDefaultFolder(folder); - folder->Release(); - } - - // Set the dialog title and option to select folders - ifd->SetOptions(FOS_PICKFOLDERS); - ifd->SetTitle(m_wtitle.c_str()); - - hr = ifd->Show(GetForegroundWindow()); - if (SUCCEEDED(hr)) - { - IShellItem* item; - hr = ifd->GetResult(&item); - if (SUCCEEDED(hr)) - { - wchar_t* wselected = nullptr; - item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); - item->Release(); - - if (wselected) - { - result = internal::wstr2str(std::wstring(wselected)); - dll ole32("ole32.dll"); - dll::proc(ole32, "CoTaskMemFree")(wselected); - } - } - } - - ifd->Release(); - - return result; - } - - std::wstring m_wtitle; - std::wstring m_wdefault_path; - - std::vector m_vector_result; -#endif -}; - -} // namespace internal - -class notify : public internal::dialog -{ -public: - 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; - - // FIXME check buffer length - lstrcpyW(nid->szInfoTitle, internal::str2wstr(title).c_str()); - lstrcpyW(nid->szInfo, internal::str2wstr(message).c_str()); - - // Display the new icon - Shell_NotifyIconW(NIM_ADD, nid.get()); -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - command += " -e 'display notification " + osascript_quote(message) + - " with title " + osascript_quote(title) + "'"; - } - else if (is_zenity()) - { - command += " --notification" - " --window-icon " + get_icon_name(icon) + - " --text " + shell_quote(title + "\n" + message); - } - else if (is_kdialog()) - { - command += " --icon " + get_icon_name(icon) + - " --title " + shell_quote(title) + - " --passivepopup " + shell_quote(message) + - " 5"; - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start(command); -#endif - } -}; - -class message : public internal::dialog -{ -public: - message(std::string const &title, - std::string const &text, - choice choice = choice::ok_cancel, - icon icon = icon::info) - { -#if _WIN32 - UINT style = MB_TOPMOST; - 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([text, title, style](int *exit_code) -> std::string - { - auto wtext = internal::str2wstr(text); - auto wtitle = internal::str2wstr(title); - // using set context to apply new visual style (required for all windows versions) - new_style_context ctx; - *exit_code = MessageBoxW(GetForegroundWindow(), wtext.c_str(), wtitle.c_str(), style); - return ""; - }); -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - command += " -e 'display dialog " + osascript_quote(text) + - " with title " + osascript_quote(title); - switch (choice) - { - case choice::ok_cancel: - command += "buttons {\"OK\", \"Cancel\"} " - "default button \"OK\" " - "cancel button \"Cancel\""; - m_mappings[256] = button::cancel; - break; - case choice::yes_no: - command += "buttons {\"Yes\", \"No\"} " - "default button \"Yes\" " - "cancel button \"No\""; - m_mappings[256] = button::no; - break; - case choice::yes_no_cancel: - command += "buttons {\"Yes\", \"No\", \"Cancel\"} " - "default button \"Yes\" " - "cancel button \"Cancel\""; - m_mappings[256] = button::cancel; - break; - case choice::retry_cancel: - command += "buttons {\"Retry\", \"Cancel\"} " - "default button \"Retry\" " - "cancel button \"Cancel\""; - m_mappings[256] = button::cancel; - break; - case choice::abort_retry_ignore: - command += "buttons {\"Abort\", \"Retry\", \"Ignore\"} " - "default button \"Retry\" " - "cancel button \"Retry\""; - m_mappings[256] = button::cancel; - break; - case choice::ok: default: - command += "buttons {\"OK\"} " - "default button \"OK\" " - "cancel button \"OK\""; - m_mappings[256] = button::ok; - break; - } - command += " 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: command += PFD_OSX_ICON("ToolBarInfo"); break; - case icon::warning: command += "caution"; break; - case icon::error: command += "stop"; break; - case icon::question: command += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; - #undef PFD_OSX_ICON - } - command += "'"; - } - else if (is_zenity()) - { - switch (choice) - { - case choice::ok_cancel: - command += " --question --ok-label=OK --cancel-label=Cancel"; 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 += " --question --switch --extra-button No --extra-button Yes"; break; - case choice::yes_no_cancel: - command += " --question --switch --extra-button No --extra-button Yes --extra-button Cancel"; break; - case choice::retry_cancel: - command += " --question --switch --extra-button Retry --extra-button Cancel"; break; - case choice::abort_retry_ignore: - command += " --question --switch --extra-button Abort --extra-button Retry --extra-button Ignore"; break; - default: - switch (icon) - { - case icon::error: command += " --error"; break; - case icon::warning: command += " --warning"; break; - default: command += " --info"; break; - } - } - - command += " --title " + shell_quote(title) - + " --width 300 --height 0" // sensible defaults - + " --text " + shell_quote(text) - + " --icon-name=dialog-" + get_icon_name(icon); - } - else if (is_kdialog()) - { - if (choice == choice::ok) - { - switch (icon) - { - case icon::error: command += " --error"; break; - case icon::warning: command += " --sorry"; break; - default: command += " --msgbox"; break; - } - } - else - { - command += " --"; - if (icon == icon::warning || icon == icon::error) - command += "warning"; - command += "yesno"; - if (choice == choice::yes_no_cancel) - command += "cancel"; - if (choice == choice::yes_no || choice == choice::yes_no_cancel) - { - m_mappings[0] = button::yes; - m_mappings[256] = button::no; - } - } - - command += " " + shell_quote(text) - + " --title " + shell_quote(title); - - // Must be after the above part - if (choice == choice::ok_cancel) - command += " --yes-label OK --no-label Cancel"; - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start(command); -#endif - } - - button 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 (exit_code < 0 || // this means cancel - 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; - } - -private: - // Some extra logic to map the exit code to button number - std::map m_mappings; -}; - -class open_file : public internal::file_dialog -{ -public: - open_file(std::string const &title, - std::string const &default_path = "", - std::vector filters = { "All Files", "*" }, - bool allow_multiselect = false) - : file_dialog(type::open, title, default_path, - filters, allow_multiselect, false) - { - } - - std::vector result() - { - return vector_result(); - } -}; - -class save_file : public internal::file_dialog -{ -public: - save_file(std::string const &title, - std::string const &default_path = "", - std::vector filters = { "All Files", "*" }, - bool confirm_overwrite = true) - : file_dialog(type::save, title, default_path, - filters, false, confirm_overwrite) - { - } - - std::string result() - { - return string_result(); - } -}; - -class select_folder : public internal::file_dialog -{ -public: - select_folder(std::string const &title, - std::string const &default_path = "") - : file_dialog(type::folder, title, default_path) - { - } - - std::string result() - { - return string_result(); - } -}; - -} // namespace pfd - diff --git a/wpigui/.styleguide b/wpigui/.styleguide index f784b5d157..9a462a3759 100644 --- a/wpigui/.styleguide +++ b/wpigui/.styleguide @@ -6,6 +6,11 @@ cppSrcFileInclude { \.cpp$ } +generatedFileExclude { + wpigui/src/main/native/cpp/portable-file-dialogs\.cpp$ + wpigui/src/main/native/include/portable-file-dialogs\.h$ +} + repoRootNameOverride { wpigui } diff --git a/wpigui/src/main/native/cpp/portable-file-dialogs.cpp b/wpigui/src/main/native/cpp/portable-file-dialogs.cpp new file mode 100644 index 0000000000..09f5d37d02 --- /dev/null +++ b/wpigui/src/main/native/cpp/portable-file-dialogs.cpp @@ -0,0 +1,1366 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +// +// Portable File Dialogs +// +// Copyright © 2018—2020 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 **** You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2() +#include // ::kill, std::signal +#include // waitpid() +#endif + +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include + +// +// Below this are all the method implementations. +// + +namespace pfd +{ + +namespace internal +{ + +class platform +{ +public: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + explicit dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast(::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 CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +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 +}; + +// internal free functions implementations + +#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; +} + +} // namespace internal + +// settings implementation + +settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + +#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 = std::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name && desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name && desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#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 +} + +void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +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 +} + +bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// executor implementation + +std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +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); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +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 +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__ +void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +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 + +internal::executor::~executor() +{ + stop(); +} + +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; +} + +void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + {} +} + +// dll implementation + +#if _WIN32 +internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// new_style_context implementation + +#if _WIN32 +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); +} + +internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +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. + internal::platform::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, + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +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 +} + +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"; + } +} + +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 +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 +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. +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 +std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +class internal::file_dialog::Impl { + public: +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); + std::string select_folder_vista(IFileDialog *ifd, bool force_path); + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +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 */) + : m_impl(std::make_shared()) +{ +#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_impl->m_wtitle = internal::str2wstr(title); + m_impl->m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Folder selection uses a different method + if (in_type == type::folder) + { + internal::platform::dll ole32("ole32.dll"); + + auto status = internal::platform::dll::proc(ole32, "CoInitializeEx") + (nullptr, COINIT_APARTMENTTHREADED); + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = internal::platform::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 m_impl->select_folder_vista(ifd, options & opt::force_path); + } + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &Impl::bffcallback; + bi.lParam = (LPARAM)m_impl.get(); + + if (flags(flag::is_vista)) + { + // This hangs on Windows XP, as reported here: + // https://github.com/samhocevar/portable-file-dialogs/pull/21 + if (status == S_OK) + 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); + internal::platform::dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + if (status == S_OK) + internal::platform::dll::proc(ole32, "CoUninitialize")(); + 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_impl->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_impl->m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_impl->m_wdefault_path.c_str(); + else if (m_impl->m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_impl->m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_impl->m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_impl->m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_impl->m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + internal::platform::dll comdlg32("comdlg32.dll"); + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + // using set context to apply new visual style (required for windows XP) + internal::platform::new_style_context ctx; + + internal::platform::dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + // using set context to apply new visual style (required for windows XP) + internal::platform::new_style_context ctx; + + internal::platform::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 += filename.size(); + // In multiselect mode, we advance p one step more 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_impl->m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#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()) + script += " default location " + 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 += (filter_list.size() == 0 ? "" : ",") + + osascript_quote(pat.substr(2, pat.size() - 2)); + } + if (has_filter && filter_list.size() > 0) + 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"); + command.push_back("--filename=" + default_path); + 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(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 +} + +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.back() == '\n' || ret.back() == '/') + ret = ret.substr(0, ret.size() - 1); + return ret; +#endif +} + +std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_impl->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 +int CALLBACK internal::file_dialog::Impl::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog::Impl *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +std::string internal::file_dialog::Impl::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) + internal::platform::dll shell32("shell32.dll"); + internal::platform::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); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wselected = nullptr; + item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); + item->Release(); + + if (wselected) + { + result = internal::wstr2str(std::wstring(wselected)); + internal::platform::dll ole32("ole32.dll"); + internal::platform::dll::proc(ole32, "CoTaskMemFree")(wselected); + } + } + } + + ifd->Release(); + + return result; +} +#endif + +// notify implementation + +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()); +#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 + +message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + UINT style = MB_TOPMOST; + 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([this, text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // using set context to apply new visual style (required for all windows versions) + internal::platform::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); + 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; + 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\"" + " cancel button \"Retry\""; + m_mappings[256] = button::cancel; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + m_mappings[256] = button::ok; + break; + } + 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 + "--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 +} + +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 (exit_code < 0 || // this means cancel + 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; +} + +} // namespace pfd diff --git a/wpigui/src/main/native/include/portable-file-dialogs.h b/wpigui/src/main/native/include/portable-file-dialogs.h new file mode 100644 index 0000000000..dffa71e196 --- /dev/null +++ b/wpigui/src/main/native/include/portable-file-dialogs.h @@ -0,0 +1,249 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 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 **** You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#include // std::string +#include // std::shared_ptr +#include // std::map +#include + +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); + + constexpr bool is_osascript() const { +#if __APPLE__ + return true; +#else + return false; +#endif + } + bool is_zenity() const { + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); + } + bool is_kdialog() const { return flags(flag::has_kdialog); } + + 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 +constexpr int default_wait_timeout = 20; + +class executor; + +class dialog : protected settings +{ +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(); + + class Impl; + std::shared_ptr m_impl; +}; + +} // namespace internal + +// +// 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) + : file_dialog(type::open, title, default_path, filters, options) + {} + + std::vector result() { return 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) + : file_dialog(type::save, title, default_path, filters, options) + {} + + std::string result() { return 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) + : file_dialog(type::folder, title, default_path, {}, options) + {} + + std::string result() { return string_result(); } +}; + +} // namespace pfd +