Path: blob/master/ext/portable-file-dialogs/portable-file-dialogs.h
3186 views
//1// Portable File Dialogs2//3// Copyright © 2018–2022 Sam Hocevar <[email protected]>4//5// This library is free software. It comes without any warranty, to6// the extent permitted by applicable law. You can redistribute it7// and/or modify it under the terms of the Do What the Fuck You Want8// to Public License, Version 2, as published by the WTFPL Task Force.9// See http://www.wtfpl.net/ for more details.10//1112#pragma once1314#if _WIN3215#ifndef WIN32_LEAN_AND_MEAN16# define WIN32_LEAN_AND_MEAN 117#endif18#include <windows.h>19#include <commdlg.h>20#include <shlobj.h>21#include <shobjidl.h> // IFileDialog22#include <shellapi.h>23#include <strsafe.h>24#include <future> // std::async25#include <userenv.h> // GetUserProfileDirectory()2627#elif __EMSCRIPTEN__28#include <emscripten.h>2930#else31#ifndef _POSIX_C_SOURCE32# define _POSIX_C_SOURCE 2 // for popen()33#endif34#ifdef __APPLE__35# ifndef _DARWIN_C_SOURCE36# define _DARWIN_C_SOURCE37# endif38#endif39#include <cstdio> // popen()40#include <cstdlib> // std::getenv()41#include <fcntl.h> // fcntl()42#include <unistd.h> // read(), pipe(), dup2(), getuid()43#include <csignal> // ::kill, std::signal44#include <sys/stat.h> // stat()45#include <sys/wait.h> // waitpid()46#include <pwd.h> // getpwnam()47#endif4849#include <string> // std::string50#include <memory> // std::shared_ptr51#include <iostream> // std::ostream52#include <map> // std::map53#include <set> // std::set54#include <regex> // std::regex55#include <thread> // std::mutex, std::this_thread56#include <chrono> // std::chrono5758// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog59#ifndef PFD_HAS_IFILEDIALOG60# define PFD_HAS_IFILEDIALOG 161# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION62# if __GXX_ABI_VERSION <= 101363# undef PFD_HAS_IFILEDIALOG64# define PFD_HAS_IFILEDIALOG 065# endif66# endif67#endif6869namespace pfd70{7172enum class button73{74cancel = -1,75ok,76yes,77no,78abort,79retry,80ignore,81};8283enum class choice84{85ok = 0,86ok_cancel,87yes_no,88yes_no_cancel,89retry_cancel,90abort_retry_ignore,91};9293enum class icon94{95info = 0,96warning,97error,98question,99};100101// Additional option flags for various dialog constructors102enum class opt : uint8_t103{104none = 0,105// For file open, allow multiselect.106multiselect = 0x1,107// For file save, force overwrite and disable the confirmation dialog.108force_overwrite = 0x2,109// For folder select, force path to be the provided argument instead110// of the last opened directory, which is the Microsoft-recommended,111// user-friendly behaviour.112force_path = 0x4,113};114115inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); }116inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); }117118// The settings class, only exposing to the user a way to set verbose mode119// and to force a rescan of installed desktop helpers (zenity, kdialog…).120class settings121{122public:123static bool available();124125static void verbose(bool value);126static void rescan();127128protected:129explicit settings(bool resync = false);130131bool check_program(std::string const &program);132133inline bool is_osascript() const;134inline bool is_zenity() const;135inline bool is_kdialog() const;136137enum class flag138{139is_scanned = 0,140is_verbose,141142has_zenity,143has_matedialog,144has_qarma,145has_kdialog,146is_vista,147148max_flag,149};150151// Static array of flags for internal state152bool const &flags(flag in_flag) const;153154// Non-const getter for the static array of flags155bool &flags(flag in_flag);156};157158// Internal classes, not to be used by client applications159namespace internal160{161162// Process wait timeout, in milliseconds163static int const default_wait_timeout = 20;164165class executor166{167friend class dialog;168169public:170// High level function to get the result of a command171std::string result(int *exit_code = nullptr);172173// High level function to abort174bool kill();175176#if _WIN32177void start_func(std::function<std::string(int *)> const &fun);178static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam);179#elif __EMSCRIPTEN__180void start(int exit_code);181#else182void start_process(std::vector<std::string> const &command);183#endif184185~executor();186187protected:188bool ready(int timeout = default_wait_timeout);189void stop();190191private:192bool m_running = false;193std::string m_stdout;194int m_exit_code = -1;195#if _WIN32196std::future<std::string> m_future;197std::set<HWND> m_windows;198std::condition_variable m_cond;199std::mutex m_mutex;200DWORD m_tid;201#elif __EMSCRIPTEN__ || __NX__202// FIXME: do something203#else204pid_t m_pid = 0;205int m_fd = -1;206#endif207};208209class platform210{211protected:212#if _WIN32213// Helper class around LoadLibraryA() and GetProcAddress() with some safety214class dll215{216public:217dll(std::string const &name);218~dll();219220template<typename T> class proc221{222public:223proc(dll const &lib, std::string const &sym)224: m_proc(reinterpret_cast<T *>((void *)::GetProcAddress(lib.handle, sym.c_str())))225{}226227operator bool() const { return m_proc != nullptr; }228operator T *() const { return m_proc; }229230private:231T *m_proc;232};233234private:235HMODULE handle;236};237238// Helper class around CoInitialize() and CoUnInitialize()239class ole32_dll : public dll240{241public:242ole32_dll();243~ole32_dll();244bool is_initialized();245246private:247HRESULT m_state;248};249250// Helper class around CreateActCtx() and ActivateActCtx()251class new_style_context252{253public:254new_style_context();255~new_style_context();256257private:258HANDLE create();259ULONG_PTR m_cookie = 0;260};261#endif262};263264class dialog : protected settings, protected platform265{266public:267bool ready(int timeout = default_wait_timeout) const;268bool kill() const;269270protected:271explicit dialog();272273std::vector<std::string> desktop_helper() const;274static std::string buttons_to_name(choice _choice);275static std::string get_icon_name(icon _icon);276277std::string powershell_quote(std::string const &str) const;278std::string osascript_quote(std::string const &str) const;279std::string shell_quote(std::string const &str) const;280281// Keep handle to executing command282std::shared_ptr<executor> m_async;283};284285class file_dialog : public dialog286{287protected:288enum type289{290open,291save,292folder,293};294295file_dialog(type in_type,296std::string const &title,297std::string const &default_path = "",298std::vector<std::string> const &filters = {},299opt options = opt::none);300301protected:302std::string string_result();303std::vector<std::string> vector_result();304305#if _WIN32306static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData);307#if PFD_HAS_IFILEDIALOG308std::string select_folder_vista(IFileDialog *ifd, bool force_path);309#endif310311std::wstring m_wtitle;312std::wstring m_wdefault_path;313314std::vector<std::string> m_vector_result;315#endif316};317318} // namespace internal319320//321// The path class provides some platform-specific path constants322//323324class path : protected internal::platform325{326public:327static std::string home();328static std::string separator();329};330331//332// The notify widget333//334335class notify : public internal::dialog336{337public:338notify(std::string const &title,339std::string const &message,340icon _icon = icon::info);341};342343//344// The message widget345//346347class message : public internal::dialog348{349public:350message(std::string const &title,351std::string const &text,352choice _choice = choice::ok_cancel,353icon _icon = icon::info);354355button result();356357private:358// Some extra logic to map the exit code to button number359std::map<int, button> m_mappings;360};361362//363// The open_file, save_file, and open_folder widgets364//365366class open_file : public internal::file_dialog367{368public:369open_file(std::string const &title,370std::string const &default_path = "",371std::vector<std::string> const &filters = { "All Files", "*" },372opt options = opt::none);373374#if defined(__has_cpp_attribute)375#if __has_cpp_attribute(deprecated)376// Backwards compatibility377[[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]]378#endif379#endif380open_file(std::string const &title,381std::string const &default_path,382std::vector<std::string> const &filters,383bool allow_multiselect);384385std::vector<std::string> result();386};387388class save_file : public internal::file_dialog389{390public:391save_file(std::string const &title,392std::string const &default_path = "",393std::vector<std::string> const &filters = { "All Files", "*" },394opt options = opt::none);395396#if defined(__has_cpp_attribute)397#if __has_cpp_attribute(deprecated)398// Backwards compatibility399[[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]]400#endif401#endif402save_file(std::string const &title,403std::string const &default_path,404std::vector<std::string> const &filters,405bool confirm_overwrite);406407std::string result();408};409410class select_folder : public internal::file_dialog411{412public:413select_folder(std::string const &title,414std::string const &default_path = "",415opt options = opt::none);416417std::string result();418};419420//421// Below this are all the method implementations. You may choose to define the422// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except423// in one place. This may reduce compilation times.424//425426#if !defined PFD_SKIP_IMPLEMENTATION427428// internal free functions implementations429430namespace internal431{432433#if _WIN32434static inline std::wstring str2wstr(std::string const &str)435{436int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0);437std::wstring ret(len, '\0');438MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size());439return ret;440}441442static inline std::string wstr2str(std::wstring const &str)443{444int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr);445std::string ret(len, '\0');446WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr);447return ret;448}449450static inline bool is_vista()451{452OSVERSIONINFOEXW osvi;453memset(&osvi, 0, sizeof(osvi));454DWORDLONG const mask = VerSetConditionMask(455VerSetConditionMask(456VerSetConditionMask(4570, VER_MAJORVERSION, VER_GREATER_EQUAL),458VER_MINORVERSION, VER_GREATER_EQUAL),459VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);460osvi.dwOSVersionInfoSize = sizeof(osvi);461osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA);462osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA);463osvi.wServicePackMajor = 0;464465return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE;466}467#endif468469// This is necessary until C++20 which will have std::string::ends_with() etc.470471static inline bool ends_with(std::string const &str, std::string const &suffix)472{473return suffix.size() <= str.size() &&474str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;475}476477static inline bool starts_with(std::string const &str, std::string const &prefix)478{479return prefix.size() <= str.size() &&480str.compare(0, prefix.size(), prefix) == 0;481}482483// This is necessary until C++17 which will have std::filesystem::is_directory484485static inline bool is_directory(std::string const &path)486{487#if _WIN32488auto attr = GetFileAttributesA(path.c_str());489return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY);490#elif __EMSCRIPTEN__491// TODO492return false;493#else494struct stat s;495return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode);496#endif497}498499// This is necessary because getenv is not thread-safe500501static inline std::string getenv(std::string const &str)502{503#if _MSC_VER504char *buf = nullptr;505size_t size = 0;506if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf)507{508std::string ret(buf);509free(buf);510return ret;511}512return "";513#else514auto buf = std::getenv(str.c_str());515return buf ? buf : "";516#endif517}518519} // namespace internal520521// settings implementation522523inline settings::settings(bool resync)524{525flags(flag::is_scanned) &= !resync;526527if (flags(flag::is_scanned))528return;529530auto pfd_verbose = internal::getenv("PFD_VERBOSE");531auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase);532if (!std::regex_match(pfd_verbose, match_no))533flags(flag::is_verbose) = true;534535#if _WIN32536flags(flag::is_vista) = internal::is_vista();537#elif !__APPLE__538flags(flag::has_zenity) = check_program("zenity");539flags(flag::has_matedialog) = check_program("matedialog");540flags(flag::has_qarma) = check_program("qarma");541flags(flag::has_kdialog) = check_program("kdialog");542543// If multiple helpers are available, try to default to the best one544if (flags(flag::has_zenity) && flags(flag::has_kdialog))545{546auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP");547if (desktop_name == std::string("gnome"))548flags(flag::has_kdialog) = false;549else if (desktop_name == std::string("KDE"))550flags(flag::has_zenity) = false;551}552#endif553554flags(flag::is_scanned) = true;555}556557inline bool settings::available()558{559#if _WIN32560return true;561#elif __APPLE__562return true;563#elif __EMSCRIPTEN__564// FIXME: Return true after implementation is complete.565return false;566#else567settings tmp;568return tmp.flags(flag::has_zenity) ||569tmp.flags(flag::has_matedialog) ||570tmp.flags(flag::has_qarma) ||571tmp.flags(flag::has_kdialog);572#endif573}574575inline void settings::verbose(bool value)576{577settings().flags(flag::is_verbose) = value;578}579580inline void settings::rescan()581{582settings(/* resync = */ true);583}584585// Check whether a program is present using “which”.586inline bool settings::check_program(std::string const &program)587{588#if _WIN32589(void)program;590return false;591#elif __EMSCRIPTEN__592(void)program;593return false;594#else595int exit_code = -1;596internal::executor async;597async.start_process({"/bin/sh", "-c", "which " + program});598async.result(&exit_code);599return exit_code == 0;600#endif601}602603inline bool settings::is_osascript() const604{605#if __APPLE__606return true;607#else608return false;609#endif610}611612inline bool settings::is_zenity() const613{614return flags(flag::has_zenity) ||615flags(flag::has_matedialog) ||616flags(flag::has_qarma);617}618619inline bool settings::is_kdialog() const620{621return flags(flag::has_kdialog);622}623624inline bool const &settings::flags(flag in_flag) const625{626static bool flags[size_t(flag::max_flag)];627return flags[size_t(in_flag)];628}629630inline bool &settings::flags(flag in_flag)631{632return const_cast<bool &>(static_cast<settings const *>(this)->flags(in_flag));633}634635// path implementation636inline std::string path::home()637{638#if _WIN32639// First try the USERPROFILE environment variable640auto user_profile = internal::getenv("USERPROFILE");641if (user_profile.size() > 0)642return user_profile;643// Otherwise, try GetUserProfileDirectory()644HANDLE token = nullptr;645DWORD len = MAX_PATH;646char buf[MAX_PATH] = { '\0' };647if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))648{649dll userenv("userenv.dll");650dll::proc<BOOL WINAPI (HANDLE, LPSTR, LPDWORD)> get_user_profile_directory(userenv, "GetUserProfileDirectoryA");651get_user_profile_directory(token, buf, &len);652CloseHandle(token);653if (*buf)654return buf;655}656#elif __EMSCRIPTEN__657return "/";658#else659// First try the HOME environment variable660auto home = internal::getenv("HOME");661if (home.size() > 0)662return home;663// Otherwise, try getpwuid_r()664size_t len = 4096;665#if defined(_SC_GETPW_R_SIZE_MAX)666auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX);667if (size_max != -1)668len = size_t(size_max);669#endif670std::vector<char> buf(len);671struct passwd pwd, *result;672if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0)673return result->pw_dir;674#endif675return "/";676}677678inline std::string path::separator()679{680#if _WIN32681return "\\";682#else683return "/";684#endif685}686687// executor implementation688689inline std::string internal::executor::result(int *exit_code /* = nullptr */)690{691stop();692if (exit_code)693*exit_code = m_exit_code;694return m_stdout;695}696697inline bool internal::executor::kill()698{699#if _WIN32700if (m_future.valid())701{702// Close all windows that weren’t open when we started the future703auto previous_windows = m_windows;704EnumWindows(&enum_windows_callback, (LPARAM)this);705for (auto hwnd : m_windows)706if (previous_windows.find(hwnd) == previous_windows.end())707{708SendMessage(hwnd, WM_CLOSE, 0, 0);709// Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox710SendMessage(hwnd, WM_COMMAND, IDNO, 0);711}712}713#elif __EMSCRIPTEN__ || __NX__714// FIXME: do something715return false; // cannot kill716#else717::kill(m_pid, SIGKILL);718#endif719stop();720return true;721}722723#if _WIN32724inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam)725{726auto that = (executor *)lParam;727728DWORD pid;729auto tid = GetWindowThreadProcessId(hwnd, &pid);730if (tid == that->m_tid)731that->m_windows.insert(hwnd);732return TRUE;733}734#endif735736#if _WIN32737inline void internal::executor::start_func(std::function<std::string(int *)> const &fun)738{739stop();740741auto trampoline = [fun, this]()742{743// Save our thread id so that the caller can cancel us744m_tid = GetCurrentThreadId();745EnumWindows(&enum_windows_callback, (LPARAM)this);746m_cond.notify_all();747return fun(&m_exit_code);748};749750std::unique_lock<std::mutex> lock(m_mutex);751m_future = std::async(std::launch::async, trampoline);752m_cond.wait(lock);753m_running = true;754}755756#elif __EMSCRIPTEN__757inline void internal::executor::start(int exit_code)758{759m_exit_code = exit_code;760}761762#else763inline void internal::executor::start_process(std::vector<std::string> const &command)764{765stop();766m_stdout.clear();767m_exit_code = -1;768769int in[2], out[2];770if (pipe(in) != 0 || pipe(out) != 0)771return;772773m_pid = fork();774if (m_pid < 0)775return;776777close(in[m_pid ? 0 : 1]);778close(out[m_pid ? 1 : 0]);779780if (m_pid == 0)781{782dup2(in[0], STDIN_FILENO);783dup2(out[1], STDOUT_FILENO);784785// Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity)786int fd = open("/dev/null", O_WRONLY);787dup2(fd, STDERR_FILENO);788close(fd);789790std::vector<char *> args;791std::transform(command.cbegin(), command.cend(), std::back_inserter(args),792[](std::string const &s) { return const_cast<char *>(s.c_str()); });793args.push_back(nullptr); // null-terminate argv[]794795execvp(args[0], args.data());796exit(1);797}798799close(in[1]);800m_fd = out[0];801auto flags = fcntl(m_fd, F_GETFL);802fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);803804m_running = true;805}806#endif807808inline internal::executor::~executor()809{810stop();811}812813inline bool internal::executor::ready(int timeout /* = default_wait_timeout */)814{815if (!m_running)816return true;817818#if _WIN32819if (m_future.valid())820{821auto status = m_future.wait_for(std::chrono::milliseconds(timeout));822if (status != std::future_status::ready)823{824// On Windows, we need to run the message pump. If the async825// thread uses a Windows API dialog, it may be attached to the826// main thread and waiting for messages that only we can dispatch.827MSG msg;828while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))829{830TranslateMessage(&msg);831DispatchMessage(&msg);832}833return false;834}835836m_stdout = m_future.get();837}838#elif __EMSCRIPTEN__ || __NX__839// FIXME: do something840(void)timeout;841#else842char buf[BUFSIZ];843ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore844if (received > 0)845{846m_stdout += std::string(buf, received);847return false;848}849850// Reap child process if it is dead. It is possible that the system has already reaped it851// (this happens when the calling application handles or ignores SIG_CHLD) and results in852// waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for853// a little while.854int status;855pid_t child = waitpid(m_pid, &status, WNOHANG);856if (child != m_pid && (child >= 0 || errno != ECHILD))857{858// FIXME: this happens almost always at first iteration859std::this_thread::sleep_for(std::chrono::milliseconds(timeout));860return false;861}862863close(m_fd);864m_exit_code = WEXITSTATUS(status);865#endif866867m_running = false;868return true;869}870871inline void internal::executor::stop()872{873// Loop until the user closes the dialog874while (!ready())875;876}877878// dll implementation879880#if _WIN32881inline internal::platform::dll::dll(std::string const &name)882: handle(::LoadLibraryA(name.c_str()))883{}884885inline internal::platform::dll::~dll()886{887if (handle)888::FreeLibrary(handle);889}890#endif // _WIN32891892// ole32_dll implementation893894#if _WIN32895inline internal::platform::ole32_dll::ole32_dll()896: dll("ole32.dll")897{898// Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes.899// See https://github.com/samhocevar/portable-file-dialogs/issues/51900auto coinit = proc<HRESULT WINAPI (LPVOID, DWORD)>(*this, "CoInitializeEx");901m_state = coinit(nullptr, COINIT_MULTITHREADED);902}903904inline internal::platform::ole32_dll::~ole32_dll()905{906if (is_initialized())907proc<void WINAPI ()>(*this, "CoUninitialize")();908}909910inline bool internal::platform::ole32_dll::is_initialized()911{912return m_state == S_OK || m_state == S_FALSE;913}914#endif915916// new_style_context implementation917918#if _WIN32919inline internal::platform::new_style_context::new_style_context()920{921// Only create one activation context for the whole app lifetime.922static HANDLE hctx = create();923924if (hctx != INVALID_HANDLE_VALUE)925ActivateActCtx(hctx, &m_cookie);926}927928inline internal::platform::new_style_context::~new_style_context()929{930DeactivateActCtx(0, m_cookie);931}932933inline HANDLE internal::platform::new_style_context::create()934{935// This “hack” seems to be necessary for this code to work on windows XP.936// Without it, dialogs do not show and close immediately. GetError()937// returns 0 so I don’t know what causes this. I was not able to reproduce938// this behavior on Windows 7 and 10 but just in case, let it be here for939// those versions too.940// This hack is not required if other dialogs are used (they load comdlg32941// automatically), only if message boxes are used.942dll comdlg32("comdlg32.dll");943944// Using approach as shown here: https://stackoverflow.com/a/10444161945UINT len = ::GetSystemDirectoryA(nullptr, 0);946std::string sys_dir(len, '\0');947::GetSystemDirectoryA(&sys_dir[0], len);948949ACTCTXA act_ctx =950{951// Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a952// crash with error “default context is already set”.953sizeof(act_ctx),954ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID,955"shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, nullptr, 0,956};957958return ::CreateActCtxA(&act_ctx);959}960#endif // _WIN32961962// dialog implementation963964inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const965{966return m_async->ready(timeout);967}968969inline bool internal::dialog::kill() const970{971return m_async->kill();972}973974inline internal::dialog::dialog()975: m_async(std::make_shared<executor>())976{977}978979inline std::vector<std::string> internal::dialog::desktop_helper() const980{981#if __APPLE__982return { "osascript" };983#else984return { flags(flag::has_zenity) ? "zenity"985: flags(flag::has_matedialog) ? "matedialog"986: flags(flag::has_qarma) ? "qarma"987: flags(flag::has_kdialog) ? "kdialog"988: "echo" };989#endif990}991992inline std::string internal::dialog::buttons_to_name(choice _choice)993{994switch (_choice)995{996case choice::ok_cancel: return "okcancel";997case choice::yes_no: return "yesno";998case choice::yes_no_cancel: return "yesnocancel";999case choice::retry_cancel: return "retrycancel";1000case choice::abort_retry_ignore: return "abortretryignore";1001/* case choice::ok: */ default: return "ok";1002}1003}10041005inline std::string internal::dialog::get_icon_name(icon _icon)1006{1007switch (_icon)1008{1009case icon::warning: return "warning";1010case icon::error: return "error";1011case icon::question: return "question";1012// Zenity wants "information" but WinForms wants "info"1013/* case icon::info: */ default:1014#if _WIN321015return "info";1016#else1017return "information";1018#endif1019}1020}10211022// This is only used for debugging purposes1023inline std::ostream& operator <<(std::ostream &s, std::vector<std::string> const &v)1024{1025int not_first = 0;1026for (auto &e : v)1027s << (not_first++ ? " " : "") << e;1028return s;1029}10301031// Properly quote a string for Powershell: replace ' or " with '' or ""1032// FIXME: we should probably get rid of newlines!1033// FIXME: the \" sequence seems unsafe, too!1034// XXX: this is no longer used but I would like to keep it around just in case1035inline std::string internal::dialog::powershell_quote(std::string const &str) const1036{1037return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'";1038}10391040// Properly quote a string for osascript: replace \ or " with \\ or \"1041// XXX: this also used to replace ' with \' when popen was used, but it would be1042// smarter to do shell_quote(osascript_quote(...)) if this is needed again.1043inline std::string internal::dialog::osascript_quote(std::string const &str) const1044{1045return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\"";1046}10471048// Properly quote a string for the shell: just replace ' with '\''1049// XXX: this is no longer used but I would like to keep it around just in case1050inline std::string internal::dialog::shell_quote(std::string const &str) const1051{1052return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'";1053}10541055// file_dialog implementation10561057inline internal::file_dialog::file_dialog(type in_type,1058std::string const &title,1059std::string const &default_path /* = "" */,1060std::vector<std::string> const &filters /* = {} */,1061opt options /* = opt::none */)1062{1063#if _WIN321064std::string filter_list;1065std::regex whitespace(" *");1066for (size_t i = 0; i + 1 < filters.size(); i += 2)1067{1068filter_list += filters[i] + '\0';1069filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0';1070}1071filter_list += '\0';10721073m_async->start_func([this, in_type, title, default_path, filter_list,1074options](int *exit_code) -> std::string1075{1076(void)exit_code;1077m_wtitle = internal::str2wstr(title);1078m_wdefault_path = internal::str2wstr(default_path);1079auto wfilter_list = internal::str2wstr(filter_list);10801081// Initialise COM. This is required for the new folder selection window,1082// (see https://github.com/samhocevar/portable-file-dialogs/pull/21)1083// and to avoid random crashes with GetOpenFileNameW() (see1084// https://github.com/samhocevar/portable-file-dialogs/issues/51)1085ole32_dll ole32;10861087// Folder selection uses a different method1088if (in_type == type::folder)1089{1090#if PFD_HAS_IFILEDIALOG1091if (flags(flag::is_vista))1092{1093// On Vista and higher we should be able to use IFileDialog for folder selection1094IFileDialog *ifd;1095HRESULT hr = dll::proc<HRESULT WINAPI (REFCLSID, LPUNKNOWN, DWORD, REFIID, LPVOID *)>(ole32, "CoCreateInstance")1096(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd));10971098// In case CoCreateInstance fails (which it should not), try legacy approach1099if (SUCCEEDED(hr))1100return select_folder_vista(ifd, options & opt::force_path);1101}1102#endif11031104BROWSEINFOW bi;1105memset(&bi, 0, sizeof(bi));11061107bi.lpfn = &bffcallback;1108bi.lParam = (LPARAM)this;11091110if (flags(flag::is_vista))1111{1112if (ole32.is_initialized())1113bi.ulFlags |= BIF_NEWDIALOGSTYLE;1114bi.ulFlags |= BIF_EDITBOX;1115bi.ulFlags |= BIF_STATUSTEXT;1116}11171118auto *list = SHBrowseForFolderW(&bi);1119std::string ret;1120if (list)1121{1122auto buffer = new wchar_t[MAX_PATH];1123SHGetPathFromIDListW(list, buffer);1124dll::proc<void WINAPI (LPVOID)>(ole32, "CoTaskMemFree")(list);1125ret = internal::wstr2str(buffer);1126delete[] buffer;1127}1128return ret;1129}11301131OPENFILENAMEW ofn;1132memset(&ofn, 0, sizeof(ofn));1133ofn.lStructSize = sizeof(OPENFILENAMEW);1134ofn.hwndOwner = GetActiveWindow();11351136ofn.lpstrFilter = wfilter_list.c_str();11371138auto woutput = std::wstring(MAX_PATH * 256, L'\0');1139ofn.lpstrFile = (LPWSTR)woutput.data();1140ofn.nMaxFile = (DWORD)woutput.size();1141if (!m_wdefault_path.empty())1142{1143// If a directory was provided, use it as the initial directory. If1144// a valid path was provided, use it as the initial file. Otherwise,1145// let the Windows API decide.1146auto path_attr = GetFileAttributesW(m_wdefault_path.c_str());1147if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY))1148ofn.lpstrInitialDir = m_wdefault_path.c_str();1149else if (m_wdefault_path.size() <= woutput.size())1150//second argument is size of buffer, not length of string1151StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str());1152else1153{1154ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data();1155ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size();1156}1157}1158ofn.lpstrTitle = m_wtitle.c_str();1159ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER;11601161dll comdlg32("comdlg32.dll");11621163// Apply new visual style (required for windows XP)1164new_style_context ctx;11651166if (in_type == type::save)1167{1168if (!(options & opt::force_overwrite))1169ofn.Flags |= OFN_OVERWRITEPROMPT;11701171dll::proc<BOOL WINAPI (LPOPENFILENAMEW)> get_save_file_name(comdlg32, "GetSaveFileNameW");1172if (get_save_file_name(&ofn) == 0)1173return "";1174return internal::wstr2str(woutput.c_str());1175}1176else1177{1178if (options & opt::multiselect)1179ofn.Flags |= OFN_ALLOWMULTISELECT;1180ofn.Flags |= OFN_PATHMUSTEXIST;11811182dll::proc<BOOL WINAPI (LPOPENFILENAMEW)> get_open_file_name(comdlg32, "GetOpenFileNameW");1183if (get_open_file_name(&ofn) == 0)1184return "";1185}11861187std::string prefix;1188for (wchar_t const *p = woutput.c_str(); *p; )1189{1190auto filename = internal::wstr2str(p);1191p += wcslen(p);1192// In multiselect mode, we advance p one wchar further and1193// check for another filename. If there is one and the1194// prefix is empty, it means we just read the prefix.1195if ((options & opt::multiselect) && *++p && prefix.empty())1196{1197prefix = filename + "/";1198continue;1199}12001201m_vector_result.push_back(prefix + filename);1202}12031204return "";1205});1206#elif __EMSCRIPTEN__1207// FIXME: do something1208(void)in_type;1209(void)title;1210(void)default_path;1211(void)filters;1212(void)options;1213#else1214auto command = desktop_helper();12151216if (is_osascript())1217{1218std::string script = "set ret to choose";1219switch (in_type)1220{1221case type::save:1222script += " file name";1223break;1224case type::open: default:1225script += " file";1226if (options & opt::multiselect)1227script += " with multiple selections allowed";1228break;1229case type::folder:1230script += " folder";1231break;1232}12331234if (default_path.size())1235{1236if (in_type == type::folder || is_directory(default_path))1237script += " default location ";1238else1239script += " default name ";1240script += osascript_quote(default_path);1241}12421243script += " with prompt " + osascript_quote(title);12441245if (in_type == type::open)1246{1247// Concatenate all user-provided filter patterns1248std::string patterns;1249for (size_t i = 0; i < filters.size() / 2; ++i)1250patterns += " " + filters[2 * i + 1];12511252// Split the pattern list to check whether "*" is in there; if it1253// is, we have to disable filters because there is no mechanism in1254// OS X for the user to override the filter.1255std::regex sep("\\s+");1256std::string filter_list;1257bool has_filter = true;1258std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1);1259std::sregex_token_iterator end;1260for ( ; iter != end; ++iter)1261{1262auto pat = iter->str();1263if (pat == "*" || pat == "*.*")1264has_filter = false;1265else if (internal::starts_with(pat, "*."))1266filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2));1267}12681269if (has_filter && filter_list.size() > 0)1270{1271// There is a weird AppleScript bug where file extensions of length != 3 are1272// ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if1273// the whole list starts with a 3-character extension, everything works again.1274// We use "///" for such an extension because we are sure it cannot appear in1275// an actual filename.1276script += " of type {\"///\"" + filter_list + "}";1277}1278}12791280if (in_type == type::open && (options & opt::multiselect))1281{1282script += "\nset s to \"\"";1283script += "\nrepeat with i in ret";1284script += "\n set s to s & (POSIX path of i) & \"\\n\"";1285script += "\nend repeat";1286script += "\ncopy s to stdout";1287}1288else1289{1290script += "\nPOSIX path of ret";1291}12921293command.push_back("-e");1294command.push_back(script);1295}1296else if (is_zenity())1297{1298command.push_back("--file-selection");12991300// If the default path is a directory, make sure it ends with "/" otherwise zenity will1301// open the file dialog in the parent directory.1302auto filename_arg = "--filename=" + default_path;1303if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path))1304filename_arg += "/";1305command.push_back(filename_arg);13061307command.push_back("--title");1308command.push_back(title);1309command.push_back("--separator=\n");13101311for (size_t i = 0; i < filters.size() / 2; ++i)1312{1313command.push_back("--file-filter");1314command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]);1315}13161317if (in_type == type::save)1318command.push_back("--save");1319if (in_type == type::folder)1320command.push_back("--directory");1321if (!(options & opt::force_overwrite))1322command.push_back("--confirm-overwrite");1323if (options & opt::multiselect)1324command.push_back("--multiple");1325}1326else if (is_kdialog())1327{1328switch (in_type)1329{1330case type::save: command.push_back("--getsavefilename"); break;1331case type::open: command.push_back("--getopenfilename"); break;1332case type::folder: command.push_back("--getexistingdirectory"); break;1333}1334if (options & opt::multiselect)1335{1336command.push_back("--multiple");1337command.push_back("--separate-output");1338}13391340command.push_back(default_path);13411342std::string filter;1343for (size_t i = 0; i < filters.size() / 2; ++i)1344filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")";1345command.push_back(filter);13461347command.push_back("--title");1348command.push_back(title);1349}13501351if (flags(flag::is_verbose))1352std::cerr << "pfd: " << command << std::endl;13531354m_async->start_process(command);1355#endif1356}13571358inline std::string internal::file_dialog::string_result()1359{1360#if _WIN321361return m_async->result();1362#else1363auto ret = m_async->result();1364// Strip potential trailing newline (zenity). Also strip trailing slash1365// added by osascript for consistency with other backends.1366while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/'))1367ret.pop_back();1368return ret;1369#endif1370}13711372inline std::vector<std::string> internal::file_dialog::vector_result()1373{1374#if _WIN321375m_async->result();1376return m_vector_result;1377#else1378std::vector<std::string> ret;1379auto result = m_async->result();1380for (;;)1381{1382// Split result along newline characters1383auto i = result.find('\n');1384if (i == 0 || i == std::string::npos)1385break;1386ret.push_back(result.substr(0, i));1387result = result.substr(i + 1, result.size());1388}1389return ret;1390#endif1391}13921393#if _WIN321394// Use a static function to pass as BFFCALLBACK for legacy folder select1395inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg,1396LPARAM, LPARAM pData)1397{1398auto inst = (file_dialog *)pData;1399switch (uMsg)1400{1401case BFFM_INITIALIZED:1402SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str());1403break;1404}1405return 0;1406}14071408#if PFD_HAS_IFILEDIALOG1409inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path)1410{1411std::string result;14121413IShellItem *folder;14141415// Load library at runtime so app doesn't link it at load time (which will fail on windows XP)1416dll shell32("shell32.dll");1417dll::proc<HRESULT WINAPI (PCWSTR, IBindCtx*, REFIID, void**)>1418create_item(shell32, "SHCreateItemFromParsingName");14191420if (!create_item)1421return "";14221423auto hr = create_item(m_wdefault_path.c_str(),1424nullptr,1425IID_PPV_ARGS(&folder));14261427// Set default folder if found. This only sets the default folder. If1428// Windows has any info about the most recently selected folder, it1429// will display it instead. Generally, calling SetFolder() to set the1430// current directory “is not a good or expected user experience and1431// should therefore be avoided”:1432// https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder1433if (SUCCEEDED(hr))1434{1435if (force_path)1436ifd->SetFolder(folder);1437else1438ifd->SetDefaultFolder(folder);1439folder->Release();1440}14411442// Set the dialog title and option to select folders1443ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);1444ifd->SetTitle(m_wtitle.c_str());14451446hr = ifd->Show(GetActiveWindow());1447if (SUCCEEDED(hr))1448{1449IShellItem* item;1450hr = ifd->GetResult(&item);1451if (SUCCEEDED(hr))1452{1453wchar_t* wname = nullptr;1454// This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try1455// to output a debug message just in case.1456if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname)))1457{1458result = internal::wstr2str(std::wstring(wname));1459dll::proc<void WINAPI (LPVOID)>(ole32_dll(), "CoTaskMemFree")(wname);1460}1461else1462{1463if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname)))1464{1465auto name = internal::wstr2str(std::wstring(wname));1466dll::proc<void WINAPI (LPVOID)>(ole32_dll(), "CoTaskMemFree")(wname);1467std::cerr << "pfd: failed to get path for " << name << std::endl;1468}1469else1470std::cerr << "pfd: item of unknown type selected" << std::endl;1471}14721473item->Release();1474}1475}14761477ifd->Release();14781479return result;1480}1481#endif1482#endif14831484// notify implementation14851486inline notify::notify(std::string const &title,1487std::string const &message,1488icon _icon /* = icon::info */)1489{1490if (_icon == icon::question) // Not supported by notifications1491_icon = icon::info;14921493#if _WIN321494// Use a static shared pointer for notify_icon so that we can delete1495// it whenever we need to display a new one, and we can also wait1496// until the program has finished running.1497struct notify_icon_data : public NOTIFYICONDATAW1498{1499~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); }1500};15011502static std::shared_ptr<notify_icon_data> nid;15031504// Release the previous notification icon, if any, and allocate a new1505// one. Note that std::make_shared() does value initialization, so there1506// is no need to memset the structure.1507nid = nullptr;1508nid = std::make_shared<notify_icon_data>();15091510// For XP support1511nid->cbSize = NOTIFYICONDATAW_V2_SIZE;1512nid->hWnd = nullptr;1513nid->uID = 0;15141515// Flag Description:1516// - NIF_ICON The hIcon member is valid.1517// - NIF_MESSAGE The uCallbackMessage member is valid.1518// - NIF_TIP The szTip member is valid.1519// - NIF_STATE The dwState and dwStateMask members are valid.1520// - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid.1521// - NIF_GUID Reserved.1522nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO;15231524// Flag Description1525// - NIIF_ERROR An error icon.1526// - NIIF_INFO An information icon.1527// - NIIF_NONE No icon.1528// - NIIF_WARNING A warning icon.1529// - NIIF_ICON_MASK Version 6.0. Reserved.1530// - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips1531switch (_icon)1532{1533case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break;1534case icon::error: nid->dwInfoFlags = NIIF_ERROR; break;1535/* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break;1536}15371538ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL1539{1540((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName);1541return false;1542};15431544nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION);1545::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get());15461547nid->uTimeout = 5000;15481549StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str());1550StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str());15511552// Display the new icon1553Shell_NotifyIconW(NIM_ADD, nid.get());1554#elif __EMSCRIPTEN__1555// FIXME: do something1556(void)title;1557(void)message;1558#else1559auto command = desktop_helper();15601561if (is_osascript())1562{1563command.push_back("-e");1564command.push_back("display notification " + osascript_quote(message) +1565" with title " + osascript_quote(title));1566}1567else if (is_zenity())1568{1569command.push_back("--notification");1570command.push_back("--window-icon");1571command.push_back(get_icon_name(_icon));1572command.push_back("--text");1573command.push_back(title + "\n" + message);1574}1575else if (is_kdialog())1576{1577command.push_back("--icon");1578command.push_back(get_icon_name(_icon));1579command.push_back("--title");1580command.push_back(title);1581command.push_back("--passivepopup");1582command.push_back(message);1583command.push_back("5");1584}15851586if (flags(flag::is_verbose))1587std::cerr << "pfd: " << command << std::endl;15881589m_async->start_process(command);1590#endif1591}15921593// message implementation15941595inline message::message(std::string const &title,1596std::string const &text,1597choice _choice /* = choice::ok_cancel */,1598icon _icon /* = icon::info */)1599{1600#if _WIN321601// Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought1602// to front. See https://github.com/samhocevar/portable-file-dialogs/issues/521603UINT style = MB_SYSTEMMODAL;1604switch (_icon)1605{1606case icon::warning: style |= MB_ICONWARNING; break;1607case icon::error: style |= MB_ICONERROR; break;1608case icon::question: style |= MB_ICONQUESTION; break;1609/* case icon::info: */ default: style |= MB_ICONINFORMATION; break;1610}16111612switch (_choice)1613{1614case choice::ok_cancel: style |= MB_OKCANCEL; break;1615case choice::yes_no: style |= MB_YESNO; break;1616case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break;1617case choice::retry_cancel: style |= MB_RETRYCANCEL; break;1618case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break;1619/* case choice::ok: */ default: style |= MB_OK; break;1620}16211622m_mappings[IDCANCEL] = button::cancel;1623m_mappings[IDOK] = button::ok;1624m_mappings[IDYES] = button::yes;1625m_mappings[IDNO] = button::no;1626m_mappings[IDABORT] = button::abort;1627m_mappings[IDRETRY] = button::retry;1628m_mappings[IDIGNORE] = button::ignore;16291630m_async->start_func([text, title, style](int* exit_code) -> std::string1631{1632auto wtext = internal::str2wstr(text);1633auto wtitle = internal::str2wstr(title);1634// Apply new visual style (required for all Windows versions)1635new_style_context ctx;1636*exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style);1637return "";1638});16391640#elif __EMSCRIPTEN__1641std::string full_message;1642switch (_icon)1643{1644case icon::warning: full_message = "⚠️"; break;1645case icon::error: full_message = "⛔"; break;1646case icon::question: full_message = "❓"; break;1647/* case icon::info: */ default: full_message = "ℹ"; break;1648}16491650full_message += ' ' + title + "\n\n" + text;16511652// This does not really start an async task; it just passes the1653// EM_ASM_INT return value to a fake start() function.1654m_async->start(EM_ASM_INT(1655{1656if ($1)1657return window.confirm(UTF8ToString($0)) ? 0 : -1;1658alert(UTF8ToString($0));1659return 0;1660}, full_message.c_str(), _choice == choice::ok_cancel));1661#else1662auto command = desktop_helper();16631664if (is_osascript())1665{1666std::string script = "display dialog " + osascript_quote(text) +1667" with title " + osascript_quote(title);1668auto if_cancel = button::cancel;1669switch (_choice)1670{1671case choice::ok_cancel:1672script += "buttons {\"OK\", \"Cancel\"}"1673" default button \"OK\""1674" cancel button \"Cancel\"";1675break;1676case choice::yes_no:1677script += "buttons {\"Yes\", \"No\"}"1678" default button \"Yes\""1679" cancel button \"No\"";1680if_cancel = button::no;1681break;1682case choice::yes_no_cancel:1683script += "buttons {\"Yes\", \"No\", \"Cancel\"}"1684" default button \"Yes\""1685" cancel button \"Cancel\"";1686break;1687case choice::retry_cancel:1688script += "buttons {\"Retry\", \"Cancel\"}"1689" default button \"Retry\""1690" cancel button \"Cancel\"";1691break;1692case choice::abort_retry_ignore:1693script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}"1694" default button \"Abort\""1695" cancel button \"Retry\"";1696if_cancel = button::retry;1697break;1698case choice::ok: default:1699script += "buttons {\"OK\"}"1700" default button \"OK\""1701" cancel button \"OK\"";1702if_cancel = button::ok;1703break;1704}1705m_mappings[1] = if_cancel;1706m_mappings[256] = if_cancel; // XXX: I think this was never correct1707script += " with icon ";1708switch (_icon)1709{1710#define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \1711"& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")"1712case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break;1713case icon::warning: script += "caution"; break;1714case icon::error: script += "stop"; break;1715case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break;1716#undef PFD_OSX_ICON1717}17181719command.push_back("-e");1720command.push_back(script);1721}1722else if (is_zenity())1723{1724switch (_choice)1725{1726case choice::ok_cancel:1727command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break;1728case choice::yes_no:1729// Do not use standard --question because it causes “No” to return -1,1730// which is inconsistent with the “Yes/No/Cancel” mode below.1731command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break;1732case choice::yes_no_cancel:1733command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break;1734case choice::retry_cancel:1735command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break;1736case choice::abort_retry_ignore:1737command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break;1738case choice::ok:1739default:1740switch (_icon)1741{1742case icon::error: command.push_back("--error"); break;1743case icon::warning: command.push_back("--warning"); break;1744default: command.push_back("--info"); break;1745}1746}17471748command.insert(command.end(), { "--title", title,1749"--width=300", "--height=0", // sensible defaults1750"--no-markup", // do not interpret text as Pango markup1751"--text", text,1752"--icon-name=dialog-" + get_icon_name(_icon) });1753}1754else if (is_kdialog())1755{1756if (_choice == choice::ok)1757{1758switch (_icon)1759{1760case icon::error: command.push_back("--error"); break;1761case icon::warning: command.push_back("--sorry"); break;1762default: command.push_back("--msgbox"); break;1763}1764}1765else1766{1767std::string flag = "--";1768if (_icon == icon::warning || _icon == icon::error)1769flag += "warning";1770flag += "yesno";1771if (_choice == choice::yes_no_cancel)1772flag += "cancel";1773command.push_back(flag);1774if (_choice == choice::yes_no || _choice == choice::yes_no_cancel)1775{1776m_mappings[0] = button::yes;1777m_mappings[256] = button::no;1778}1779}17801781command.push_back(text);1782command.push_back("--title");1783command.push_back(title);17841785// Must be after the above part1786if (_choice == choice::ok_cancel)1787command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" });1788}17891790if (flags(flag::is_verbose))1791std::cerr << "pfd: " << command << std::endl;17921793m_async->start_process(command);1794#endif1795}17961797inline button message::result()1798{1799int exit_code;1800auto ret = m_async->result(&exit_code);1801// osascript will say "button returned:Cancel\n"1802// and others will just say "Cancel\n"1803if (internal::ends_with(ret, "Cancel\n"))1804return button::cancel;1805if (internal::ends_with(ret, "OK\n"))1806return button::ok;1807if (internal::ends_with(ret, "Yes\n"))1808return button::yes;1809if (internal::ends_with(ret, "No\n"))1810return button::no;1811if (internal::ends_with(ret, "Abort\n"))1812return button::abort;1813if (internal::ends_with(ret, "Retry\n"))1814return button::retry;1815if (internal::ends_with(ret, "Ignore\n"))1816return button::ignore;1817if (m_mappings.count(exit_code) != 0)1818return m_mappings[exit_code];1819return exit_code == 0 ? button::ok : button::cancel;1820}18211822// open_file implementation18231824inline open_file::open_file(std::string const &title,1825std::string const &default_path /* = "" */,1826std::vector<std::string> const &filters /* = { "All Files", "*" } */,1827opt options /* = opt::none */)1828: file_dialog(type::open, title, default_path, filters, options)1829{1830}18311832inline open_file::open_file(std::string const &title,1833std::string const &default_path,1834std::vector<std::string> const &filters,1835bool allow_multiselect)1836: open_file(title, default_path, filters,1837(allow_multiselect ? opt::multiselect : opt::none))1838{1839}18401841inline std::vector<std::string> open_file::result()1842{1843return vector_result();1844}18451846// save_file implementation18471848inline save_file::save_file(std::string const &title,1849std::string const &default_path /* = "" */,1850std::vector<std::string> const &filters /* = { "All Files", "*" } */,1851opt options /* = opt::none */)1852: file_dialog(type::save, title, default_path, filters, options)1853{1854}18551856inline save_file::save_file(std::string const &title,1857std::string const &default_path,1858std::vector<std::string> const &filters,1859bool confirm_overwrite)1860: save_file(title, default_path, filters,1861(confirm_overwrite ? opt::none : opt::force_overwrite))1862{1863}18641865inline std::string save_file::result()1866{1867return string_result();1868}18691870// select_folder implementation18711872inline select_folder::select_folder(std::string const &title,1873std::string const &default_path /* = "" */,1874opt options /* = opt::none */)1875: file_dialog(type::folder, title, default_path, {}, options)1876{1877}18781879inline std::string select_folder::result()1880{1881return string_result();1882}18831884#endif // PFD_SKIP_IMPLEMENTATION18851886} // namespace pfd188718881889