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