feat(dialogs): add cross-platform filesystem dialogs
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
|||||||
# ---> C++
|
# ---> C++
|
||||||
|
# CMake build trees
|
||||||
|
build/
|
||||||
|
cmake-build-*/
|
||||||
|
CMakeUserPresets.json
|
||||||
|
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
|
|
||||||
|
|||||||
70
CMakeLists.txt
Normal file
70
CMakeLists.txt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
project(iZo VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
add_library(izo STATIC)
|
||||||
|
add_library(iZo::izo ALIAS izo)
|
||||||
|
|
||||||
|
target_sources(izo
|
||||||
|
PRIVATE
|
||||||
|
src/dialogs.cpp
|
||||||
|
$<$<PLATFORM_ID:Windows>:src/dialogs_windows.cpp>
|
||||||
|
$<$<PLATFORM_ID:Linux>:src/dialogs_linux.cpp>
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(izo
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
)
|
||||||
|
target_compile_features(izo PUBLIC cxx_std_17)
|
||||||
|
set_target_properties(izo PROPERTIES
|
||||||
|
CXX_EXTENSIONS OFF
|
||||||
|
OUTPUT_NAME izo
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_compile_definitions(izo PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
|
target_link_libraries(izo PRIVATE ole32 shell32 uuid)
|
||||||
|
elseif(UNIX AND NOT APPLE)
|
||||||
|
# No link-time desktop dependency. The Linux backend discovers zenity or
|
||||||
|
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "iZo currently supports Windows and Linux")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
include(CMakePackageConfigHelpers)
|
||||||
|
|
||||||
|
install(TARGETS izo EXPORT iZoTargets
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
|
)
|
||||||
|
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
|
install(EXPORT iZoTargets
|
||||||
|
FILE iZoTargets.cmake
|
||||||
|
NAMESPACE iZo::
|
||||||
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iZo
|
||||||
|
)
|
||||||
|
|
||||||
|
write_basic_package_version_file(
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/iZoConfigVersion.cmake"
|
||||||
|
VERSION ${PROJECT_VERSION}
|
||||||
|
COMPATIBILITY SameMajorVersion
|
||||||
|
)
|
||||||
|
configure_package_config_file(
|
||||||
|
cmake/iZoConfig.cmake.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/iZoConfig.cmake"
|
||||||
|
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iZo
|
||||||
|
)
|
||||||
|
install(FILES
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/iZoConfig.cmake"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/iZoConfigVersion.cmake"
|
||||||
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iZo
|
||||||
|
)
|
||||||
|
|
||||||
|
option(IZO_BUILD_EXAMPLE "Build the iZo example program" OFF)
|
||||||
|
if(IZO_BUILD_EXAMPLE)
|
||||||
|
add_executable(izo_example examples/dialogs.cpp)
|
||||||
|
target_link_libraries(izo_example PRIVATE iZo::izo)
|
||||||
|
endif()
|
||||||
48
README.md
48
README.md
@@ -1,3 +1,49 @@
|
|||||||
# iZo
|
# iZo
|
||||||
|
|
||||||
OS IO Interface.
|
iZo is a small C++17 static library for native file-system dialogs on Windows
|
||||||
|
and Linux.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Open file, including multi-select and file filters
|
||||||
|
- Save file
|
||||||
|
- Pick folder
|
||||||
|
- Open a path with its default application
|
||||||
|
- Reveal a path in Explorer or the desktop file manager
|
||||||
|
- Explicit selected, cancelled, and error result states
|
||||||
|
|
||||||
|
Windows uses the native Explorer `IFileDialog` API. Linux discovers `zenity` or
|
||||||
|
`kdialog` at runtime; at least one must be installed for dialogs. Path opening
|
||||||
|
uses `xdg-open`. Revealing uses the FreeDesktop file-manager D-Bus interface
|
||||||
|
when available and otherwise opens the parent folder.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -DIZO_BUILD_EXAMPLE=ON
|
||||||
|
cmake --build build
|
||||||
|
```
|
||||||
|
|
||||||
|
To consume an installed copy:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
find_package(iZo CONFIG REQUIRED)
|
||||||
|
target_link_libraries(your_target PRIVATE iZo::izo)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <izo/dialogs.hpp>
|
||||||
|
|
||||||
|
izo::dialog_options options;
|
||||||
|
options.title = "Choose an image";
|
||||||
|
options.filters = {{"Images", {"*.png", "*.jpg"}}};
|
||||||
|
|
||||||
|
auto result = izo::open_file(options);
|
||||||
|
if (result) {
|
||||||
|
auto selected_path = result.paths.front();
|
||||||
|
} else if (result.status == izo::dialog_status::error) {
|
||||||
|
// result.error_message contains the platform failure.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
3
cmake/iZoConfig.cmake.in
Normal file
3
cmake/iZoConfig.cmake.in
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@PACKAGE_INIT@
|
||||||
|
|
||||||
|
include("${CMAKE_CURRENT_LIST_DIR}/iZoTargets.cmake")
|
||||||
23
examples/dialogs.cpp
Normal file
23
examples/dialogs.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include <izo/dialogs.hpp>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
izo::dialog_options options;
|
||||||
|
options.title = "Choose an image";
|
||||||
|
options.filters = {
|
||||||
|
{"Images", {"*.png", "*.jpg", "*.jpeg"}},
|
||||||
|
{"All files", {"*"}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto result = izo::open_file(options);
|
||||||
|
if (result) {
|
||||||
|
std::cout << result.paths.front().string() << '\n';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (result.status == izo::dialog_status::error) {
|
||||||
|
std::cerr << result.error_message << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
50
include/izo/dialogs.hpp
Normal file
50
include/izo/dialogs.hpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace izo {
|
||||||
|
|
||||||
|
enum class dialog_status {
|
||||||
|
selected,
|
||||||
|
cancelled,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct file_filter {
|
||||||
|
std::string name;
|
||||||
|
// Glob patterns, for example {"*.png", "*.jpg"}.
|
||||||
|
std::vector<std::string> patterns;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct dialog_options {
|
||||||
|
std::string title;
|
||||||
|
std::filesystem::path initial_path;
|
||||||
|
std::vector<file_filter> filters;
|
||||||
|
bool allow_multiple = false;
|
||||||
|
bool show_hidden = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct dialog_result {
|
||||||
|
dialog_status status = dialog_status::cancelled;
|
||||||
|
std::vector<std::filesystem::path> paths;
|
||||||
|
std::string error_message;
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept { return status == dialog_status::selected; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// These calls block until the user closes the native desktop dialog.
|
||||||
|
dialog_result open_file(const dialog_options& options = {});
|
||||||
|
dialog_result save_file(const dialog_options& options = {});
|
||||||
|
dialog_result pick_folder(const dialog_options& options = {});
|
||||||
|
|
||||||
|
// Opens a file/folder using the operating system's registered default handler.
|
||||||
|
// Returns false and optionally fills error_message if launching it failed.
|
||||||
|
bool open_path(const std::filesystem::path& path, std::string* error_message = nullptr);
|
||||||
|
|
||||||
|
// Opens the containing file manager and selects path when the platform supports it.
|
||||||
|
bool reveal_in_file_manager(const std::filesystem::path& path,
|
||||||
|
std::string* error_message = nullptr);
|
||||||
|
|
||||||
|
} // namespace izo
|
||||||
5
src/dialogs.cpp
Normal file
5
src/dialogs.cpp
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#include <izo/dialogs.hpp>
|
||||||
|
|
||||||
|
// Platform implementations live in dialogs_windows.cpp and dialogs_linux.cpp.
|
||||||
|
// This translation unit ensures the archive always has a platform-independent
|
||||||
|
// object and is a natural home for future shared helpers.
|
||||||
204
src/dialogs_linux.cpp
Normal file
204
src/dialogs_linux.cpp
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#include <izo/dialogs.hpp>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace izo {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct process_result {
|
||||||
|
int exit_code = -1;
|
||||||
|
std::string output;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool executable_on_path(const char* name) {
|
||||||
|
const char* path = std::getenv("PATH");
|
||||||
|
if (!path) return false;
|
||||||
|
std::stringstream stream(path);
|
||||||
|
std::string directory;
|
||||||
|
while (std::getline(stream, directory, ':')) {
|
||||||
|
if (directory.empty()) directory = ".";
|
||||||
|
if (access((directory + '/' + name).c_str(), X_OK) == 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_result run(const std::vector<std::string>& arguments) {
|
||||||
|
int output_pipe[2];
|
||||||
|
if (pipe(output_pipe) != 0) return {-1, {}, std::strerror(errno)};
|
||||||
|
|
||||||
|
const pid_t child = fork();
|
||||||
|
if (child < 0) {
|
||||||
|
close(output_pipe[0]); close(output_pipe[1]);
|
||||||
|
return {-1, {}, std::strerror(errno)};
|
||||||
|
}
|
||||||
|
if (child == 0) {
|
||||||
|
close(output_pipe[0]);
|
||||||
|
dup2(output_pipe[1], STDOUT_FILENO);
|
||||||
|
dup2(output_pipe[1], STDERR_FILENO);
|
||||||
|
close(output_pipe[1]);
|
||||||
|
std::vector<char*> argv;
|
||||||
|
argv.reserve(arguments.size() + 1);
|
||||||
|
for (const auto& argument : arguments) argv.push_back(const_cast<char*>(argument.c_str()));
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
execvp(argv[0], argv.data());
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(output_pipe[1]);
|
||||||
|
std::string output;
|
||||||
|
char buffer[4096];
|
||||||
|
ssize_t count;
|
||||||
|
while ((count = read(output_pipe[0], buffer, sizeof(buffer))) > 0) {
|
||||||
|
output.append(buffer, static_cast<std::size_t>(count));
|
||||||
|
}
|
||||||
|
close(output_pipe[0]);
|
||||||
|
int status = 0;
|
||||||
|
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
||||||
|
const int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||||
|
return {exit_code, std::move(output), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string initial_value(const dialog_options& options, bool save) {
|
||||||
|
if (options.initial_path.empty()) return {};
|
||||||
|
auto value = options.initial_path.string();
|
||||||
|
std::error_code ec;
|
||||||
|
if (!save && std::filesystem::is_directory(options.initial_path, ec) &&
|
||||||
|
!value.empty() && value.back() != '/') value.push_back('/');
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result parse_dialog_output(process_result process, char separator) {
|
||||||
|
if (process.exit_code == 1) return {dialog_status::cancelled, {}, {}};
|
||||||
|
if (process.exit_code != 0) {
|
||||||
|
std::string message = process.error.empty() ? process.output : process.error;
|
||||||
|
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back();
|
||||||
|
if (message.empty()) message = "Desktop dialog process failed with code " +
|
||||||
|
std::to_string(process.exit_code);
|
||||||
|
return {dialog_status::error, {}, std::move(message)};
|
||||||
|
}
|
||||||
|
while (!process.output.empty() &&
|
||||||
|
(process.output.back() == '\n' || process.output.back() == '\r')) process.output.pop_back();
|
||||||
|
if (process.output.empty()) return {dialog_status::cancelled, {}, {}};
|
||||||
|
dialog_result result{dialog_status::selected, {}, {}};
|
||||||
|
std::stringstream stream(process.output);
|
||||||
|
std::string path;
|
||||||
|
while (std::getline(stream, path, separator)) {
|
||||||
|
if (!path.empty()) result.paths.emplace_back(std::move(path));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result zenity_dialog(const dialog_options& options, bool save, bool folder) {
|
||||||
|
constexpr char separator = '\x1f';
|
||||||
|
std::vector<std::string> args{"zenity", "--file-selection"};
|
||||||
|
if (save) args.emplace_back("--save");
|
||||||
|
if (folder) args.emplace_back("--directory");
|
||||||
|
if (!save && options.allow_multiple) {
|
||||||
|
args.emplace_back("--multiple");
|
||||||
|
args.emplace_back(std::string("--separator=") + separator);
|
||||||
|
}
|
||||||
|
if (options.show_hidden) args.emplace_back("--show-hidden");
|
||||||
|
if (!options.title.empty()) args.emplace_back("--title=" + options.title);
|
||||||
|
const auto initial = initial_value(options, save);
|
||||||
|
if (!initial.empty()) args.emplace_back("--filename=" + initial);
|
||||||
|
for (const auto& filter : options.filters) {
|
||||||
|
std::string value = "--file-filter=" + filter.name + " |";
|
||||||
|
for (const auto& pattern : filter.patterns) value += ' ' + pattern;
|
||||||
|
args.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
return parse_dialog_output(run(args), separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result kdialog_dialog(const dialog_options& options, bool save, bool folder) {
|
||||||
|
std::vector<std::string> args{"kdialog"};
|
||||||
|
if (folder) args.emplace_back("--getexistingdirectory");
|
||||||
|
else if (save) args.emplace_back("--getsavefilename");
|
||||||
|
else args.emplace_back("--getopenfilename");
|
||||||
|
const auto initial = initial_value(options, save);
|
||||||
|
args.push_back(initial.empty() ? std::string(".") : initial);
|
||||||
|
if (!folder && !options.filters.empty()) {
|
||||||
|
std::string filters;
|
||||||
|
for (const auto& filter : options.filters) {
|
||||||
|
if (!filters.empty()) filters += "\n";
|
||||||
|
for (std::size_t i = 0; i < filter.patterns.size(); ++i) {
|
||||||
|
if (i) filters += ' ';
|
||||||
|
filters += filter.patterns[i];
|
||||||
|
}
|
||||||
|
filters += '|' + filter.name;
|
||||||
|
}
|
||||||
|
args.push_back(std::move(filters));
|
||||||
|
}
|
||||||
|
if (!options.title.empty()) { args.emplace_back("--title"); args.push_back(options.title); }
|
||||||
|
if (!save && options.allow_multiple) {
|
||||||
|
args.emplace_back("--multiple");
|
||||||
|
args.emplace_back("--separate-output");
|
||||||
|
}
|
||||||
|
return parse_dialog_output(run(args), '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result show_dialog(const dialog_options& options, bool save, bool folder) {
|
||||||
|
if (executable_on_path("zenity")) return zenity_dialog(options, save, folder);
|
||||||
|
if (executable_on_path("kdialog")) return kdialog_dialog(options, save, folder);
|
||||||
|
return {dialog_status::error, {},
|
||||||
|
"No supported desktop dialog provider found; install zenity or kdialog"};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool launch_and_report(const std::vector<std::string>& args, std::string* error_message) {
|
||||||
|
auto result = run(args);
|
||||||
|
if (result.exit_code == 0) return true;
|
||||||
|
if (error_message) {
|
||||||
|
*error_message = result.output.empty()
|
||||||
|
? "Process failed with code " + std::to_string(result.exit_code)
|
||||||
|
: std::move(result.output);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string file_uri(const std::filesystem::path& path) {
|
||||||
|
// D-Bus accepts file URIs. Percent-encode bytes outside the unreserved URI set.
|
||||||
|
const auto absolute = std::filesystem::absolute(path).string();
|
||||||
|
std::ostringstream uri;
|
||||||
|
uri << "file://";
|
||||||
|
constexpr char hex[] = "0123456789ABCDEF";
|
||||||
|
for (const unsigned char c : absolute) {
|
||||||
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '/' || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||||
|
uri << static_cast<char>(c);
|
||||||
|
} else {
|
||||||
|
uri << '%' << hex[c >> 4] << hex[c & 15];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
dialog_result open_file(const dialog_options& options) { return show_dialog(options, false, false); }
|
||||||
|
dialog_result save_file(const dialog_options& options) { return show_dialog(options, true, false); }
|
||||||
|
dialog_result pick_folder(const dialog_options& options) { return show_dialog(options, false, true); }
|
||||||
|
|
||||||
|
bool open_path(const std::filesystem::path& path, std::string* error_message) {
|
||||||
|
return launch_and_report({"xdg-open", path.string()}, error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reveal_in_file_manager(const std::filesystem::path& path, std::string* error_message) {
|
||||||
|
if (executable_on_path("dbus-send")) {
|
||||||
|
auto result = run({"dbus-send", "--session", "--dest=org.freedesktop.FileManager1",
|
||||||
|
"--type=method_call", "/org/freedesktop/FileManager1",
|
||||||
|
"org.freedesktop.FileManager1.ShowItems",
|
||||||
|
"array:string:" + file_uri(path), "string:"});
|
||||||
|
if (result.exit_code == 0) return true;
|
||||||
|
}
|
||||||
|
const auto parent = std::filesystem::is_directory(path) ? path : path.parent_path();
|
||||||
|
return open_path(parent.empty() ? std::filesystem::path(".") : parent, error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace izo
|
||||||
231
src/dialogs_windows.cpp
Normal file
231
src/dialogs_windows.cpp
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include <izo/dialogs.hpp>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shobjidl.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace izo {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class com_ptr {
|
||||||
|
public:
|
||||||
|
~com_ptr() { reset(); }
|
||||||
|
com_ptr() = default;
|
||||||
|
com_ptr(const com_ptr&) = delete;
|
||||||
|
com_ptr& operator=(const com_ptr&) = delete;
|
||||||
|
T** put() { reset(); return &value_; }
|
||||||
|
T* get() const { return value_; }
|
||||||
|
T* operator->() const { return value_; }
|
||||||
|
void reset() { if (value_) value_->Release(); value_ = nullptr; }
|
||||||
|
private:
|
||||||
|
T* value_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::wstring widen(const std::string& value) {
|
||||||
|
if (value.empty()) return {};
|
||||||
|
const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||||
|
value.data(), static_cast<int>(value.size()),
|
||||||
|
nullptr, 0);
|
||||||
|
if (count <= 0) return {};
|
||||||
|
std::wstring output(static_cast<std::size_t>(count), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
|
||||||
|
static_cast<int>(value.size()), output.data(), count);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string narrow(const std::wstring& value) {
|
||||||
|
if (value.empty()) return {};
|
||||||
|
const int count = WideCharToMultiByte(CP_UTF8, 0, value.data(),
|
||||||
|
static_cast<int>(value.size()), nullptr, 0,
|
||||||
|
nullptr, nullptr);
|
||||||
|
std::string output(static_cast<std::size_t>(count), '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()),
|
||||||
|
output.data(), count, nullptr, nullptr);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hresult_message(const char* operation, HRESULT hr) {
|
||||||
|
wchar_t* buffer = nullptr;
|
||||||
|
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, static_cast<DWORD>(hr), 0,
|
||||||
|
reinterpret_cast<wchar_t*>(&buffer), 0, nullptr);
|
||||||
|
std::ostringstream message;
|
||||||
|
message << operation << " failed (0x" << std::hex << static_cast<unsigned long>(hr) << ')';
|
||||||
|
if (buffer) {
|
||||||
|
std::wstring detail(buffer);
|
||||||
|
LocalFree(buffer);
|
||||||
|
while (!detail.empty() && (detail.back() == L'\r' || detail.back() == L'\n')) {
|
||||||
|
detail.pop_back();
|
||||||
|
}
|
||||||
|
message << ": " << narrow(detail);
|
||||||
|
}
|
||||||
|
return message.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result failure(const char* operation, HRESULT hr) {
|
||||||
|
return {dialog_status::error, {}, hresult_message(operation, hr)};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool add_shell_item_path(IShellItem* item, dialog_result& result) {
|
||||||
|
wchar_t* path = nullptr;
|
||||||
|
const HRESULT hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
result = failure("Reading selected path", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
result.paths.emplace_back(path);
|
||||||
|
CoTaskMemFree(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result show_dialog(const dialog_options& options, bool save, bool folder) {
|
||||||
|
const HRESULT init_hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||||
|
if (init_hr == RPC_E_CHANGED_MODE) {
|
||||||
|
// IFileDialog is an apartment-threaded COM component. If the caller
|
||||||
|
// already initialized this thread as MTA, host the modal dialog on a
|
||||||
|
// temporary STA instead of disturbing the caller's COM configuration.
|
||||||
|
dialog_result threaded_result;
|
||||||
|
std::thread dialog_thread([&] { threaded_result = show_dialog(options, save, folder); });
|
||||||
|
dialog_thread.join();
|
||||||
|
return threaded_result;
|
||||||
|
}
|
||||||
|
const bool uninitialize = SUCCEEDED(init_hr);
|
||||||
|
if (FAILED(init_hr)) {
|
||||||
|
return failure("COM initialization", init_hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_result result;
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
com_ptr<IFileDialog> dialog;
|
||||||
|
if (save) {
|
||||||
|
hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(dialog.put()));
|
||||||
|
} else {
|
||||||
|
hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(dialog.put()));
|
||||||
|
}
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
result = failure("Creating file dialog", hr);
|
||||||
|
} else {
|
||||||
|
FILEOPENDIALOGOPTIONS flags = 0;
|
||||||
|
hr = dialog->GetOptions(&flags);
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
flags |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
|
||||||
|
if (!save && !folder) flags |= FOS_FILEMUSTEXIST;
|
||||||
|
if (folder) flags |= FOS_PICKFOLDERS;
|
||||||
|
if (!save && options.allow_multiple) flags |= FOS_ALLOWMULTISELECT;
|
||||||
|
if (options.show_hidden) flags |= FOS_FORCESHOWHIDDEN;
|
||||||
|
hr = dialog->SetOptions(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto title = widen(options.title);
|
||||||
|
if (SUCCEEDED(hr) && !title.empty()) hr = dialog->SetTitle(title.c_str());
|
||||||
|
|
||||||
|
std::vector<std::wstring> names;
|
||||||
|
std::vector<std::wstring> patterns;
|
||||||
|
std::vector<COMDLG_FILTERSPEC> specs;
|
||||||
|
names.reserve(options.filters.size());
|
||||||
|
patterns.reserve(options.filters.size());
|
||||||
|
for (const auto& filter : options.filters) {
|
||||||
|
names.push_back(widen(filter.name));
|
||||||
|
std::wstring joined;
|
||||||
|
for (const auto& pattern : filter.patterns) {
|
||||||
|
if (!joined.empty()) joined += L';';
|
||||||
|
joined += widen(pattern);
|
||||||
|
}
|
||||||
|
patterns.push_back(std::move(joined));
|
||||||
|
}
|
||||||
|
specs.reserve(names.size());
|
||||||
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||||
|
specs.push_back({names[i].c_str(), patterns[i].c_str()});
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(hr) && !specs.empty()) {
|
||||||
|
hr = dialog->SetFileTypes(static_cast<UINT>(specs.size()), specs.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr) && !options.initial_path.empty()) {
|
||||||
|
std::filesystem::path folder_path = options.initial_path;
|
||||||
|
std::error_code ec;
|
||||||
|
const bool initial_is_directory = std::filesystem::is_directory(folder_path, ec);
|
||||||
|
if (!initial_is_directory) folder_path = folder_path.parent_path();
|
||||||
|
if (!folder_path.empty()) {
|
||||||
|
com_ptr<IShellItem> initial;
|
||||||
|
const auto native = folder_path.wstring();
|
||||||
|
if (SUCCEEDED(SHCreateItemFromParsingName(native.c_str(), nullptr,
|
||||||
|
IID_PPV_ARGS(initial.put())))) {
|
||||||
|
dialog->SetFolder(initial.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (save && !initial_is_directory && options.initial_path.has_filename()) {
|
||||||
|
dialog->SetFileName(options.initial_path.filename().wstring().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) hr = dialog->Show(nullptr);
|
||||||
|
if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
|
||||||
|
result.status = dialog_status::cancelled;
|
||||||
|
} else if (FAILED(hr)) {
|
||||||
|
result = failure("Showing file dialog", hr);
|
||||||
|
} else if (!save && options.allow_multiple) {
|
||||||
|
com_ptr<IFileOpenDialog> open_dialog;
|
||||||
|
hr = dialog->QueryInterface(IID_PPV_ARGS(open_dialog.put()));
|
||||||
|
com_ptr<IShellItemArray> items;
|
||||||
|
if (SUCCEEDED(hr)) hr = open_dialog->GetResults(items.put());
|
||||||
|
DWORD count = 0;
|
||||||
|
if (SUCCEEDED(hr)) hr = items->GetCount(&count);
|
||||||
|
result.status = dialog_status::selected;
|
||||||
|
for (DWORD i = 0; SUCCEEDED(hr) && i < count; ++i) {
|
||||||
|
com_ptr<IShellItem> item;
|
||||||
|
hr = items->GetItemAt(i, item.put());
|
||||||
|
if (SUCCEEDED(hr) && !add_shell_item_path(item.get(), result)) hr = E_FAIL;
|
||||||
|
}
|
||||||
|
if (FAILED(hr) && result.status != dialog_status::error) result = failure("Reading selections", hr);
|
||||||
|
} else {
|
||||||
|
com_ptr<IShellItem> item;
|
||||||
|
hr = dialog->GetResult(item.put());
|
||||||
|
if (FAILED(hr)) result = failure("Reading selection", hr);
|
||||||
|
else {
|
||||||
|
result.status = dialog_status::selected;
|
||||||
|
add_shell_item_path(item.get(), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.reset();
|
||||||
|
if (uninitialize) CoUninitialize();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shell_launch(const wchar_t* verb, const std::filesystem::path& target,
|
||||||
|
const wchar_t* parameters, std::string* error_message) {
|
||||||
|
const auto result = reinterpret_cast<std::intptr_t>(
|
||||||
|
ShellExecuteW(nullptr, verb, target.c_str(), parameters, nullptr, SW_SHOWNORMAL));
|
||||||
|
if (result > 32) return true;
|
||||||
|
if (error_message) {
|
||||||
|
*error_message = "Shell launch failed with code " + std::to_string(result);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
dialog_result open_file(const dialog_options& options) { return show_dialog(options, false, false); }
|
||||||
|
dialog_result save_file(const dialog_options& options) { return show_dialog(options, true, false); }
|
||||||
|
dialog_result pick_folder(const dialog_options& options) { return show_dialog(options, false, true); }
|
||||||
|
|
||||||
|
bool open_path(const std::filesystem::path& path, std::string* error_message) {
|
||||||
|
return shell_launch(L"open", path, nullptr, error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reveal_in_file_manager(const std::filesystem::path& path, std::string* error_message) {
|
||||||
|
const std::wstring parameters = L"/select,\"" + path.wstring() + L"\"";
|
||||||
|
return shell_launch(L"open", L"explorer.exe", parameters.c_str(), error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace izo
|
||||||
Reference in New Issue
Block a user