From f9365481b97646999fa96ed31c28ad8f53aa068a Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 20 Aug 2025 21:59:51 +0800 Subject: [PATCH] fix: fix all build issue of dialog namespace but not test. --- .clang-format | 3 + src/CMakeLists.txt | 4 +- src/yycc/macro/class_copy_move.hpp | 39 +- src/yycc/windows/dialog.cpp | 646 ++++++++++++++++++++--------- src/yycc/windows/dialog.hpp | 298 ++++++------- 5 files changed, 649 insertions(+), 341 deletions(-) diff --git a/.clang-format b/.clang-format index 062d306..22c0166 100644 --- a/.clang-format +++ b/.clang-format @@ -300,6 +300,9 @@ StatementMacros: - YYCC_DEFAULT_COPY - YYCC_DEFAULT_MOVE - YYCC_DEFAULT_COPY_MOVE + - YYCC_DECL_COPY + - YYCC_DECL_MOVE + - YYCC_DECL_COPY_MOVE TableGenBreakInsideDAGArg: DontBreak TabWidth: 4 UseTab: Never diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0b106ca..384a802 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,7 +17,7 @@ PRIVATE yycc/rust/panic.cpp yycc/rust/env.cpp yycc/windows/com.cpp - #yycc/windows/dialog.cpp + yycc/windows/dialog.cpp yycc/windows/winfct.cpp yycc/encoding/stl.cpp yycc/encoding/windows.cpp @@ -61,7 +61,7 @@ FILES yycc/windows/import_guard_head.hpp yycc/windows/import_guard_tail.hpp yycc/windows/com.hpp - #yycc/windows/dialog.hpp + yycc/windows/dialog.hpp yycc/windows/winfct.hpp yycc/constraint.hpp yycc/constraint/builder.hpp diff --git a/src/yycc/macro/class_copy_move.hpp b/src/yycc/macro/class_copy_move.hpp index 3e2b56f..c539aaa 100644 --- a/src/yycc/macro/class_copy_move.hpp +++ b/src/yycc/macro/class_copy_move.hpp @@ -7,8 +7,8 @@ /// @brief Explicitly remove move (\c constructor and \c operator\=) for given class. #define YYCC_DELETE_MOVE(CLSNAME) \ - CLSNAME(CLSNAME&&) = delete; \ - CLSNAME& operator=(CLSNAME&&) = delete; + CLSNAME(CLSNAME&&) noexcept = delete; \ + CLSNAME& operator=(CLSNAME&&) noexcept = delete; /// @brief Explicitly remove (copy and move) (\c constructor and \c operator\=) for given class. #define YYCC_DELETE_COPY_MOVE(CLSNAME) \ @@ -22,10 +22,41 @@ /// @brief Explicitly set default move (\c constructor and \c operator\=) for given class. #define YYCC_DEFAULT_MOVE(CLSNAME) \ - CLSNAME(CLSNAME&&) = default; \ - CLSNAME& operator=(CLSNAME&&) = default; + CLSNAME(CLSNAME&&) noexcept = default; \ + CLSNAME& operator=(CLSNAME&&) noexcept = default; /// @brief Explicitly set default (copy and move) (\c constructor and \c operator\=) for given class. #define YYCC_DEFAULT_COPY_MOVE(CLSNAME) \ YYCC_DEFAULT_COPY(CLSNAME) \ YYCC_DEFAULT_MOVE(CLSNAME) + +/// @brief Make declaration of copy (\c constructor and \c operator\=) for given class to avoid typo. +#define YYCC_DECL_COPY(CLSNAME) \ + CLSNAME(const CLSNAME&); \ + CLSNAME& operator=(const CLSNAME&); + +/// @brief Make declaration of move (\c constructor and \c operator\=) for given class to avoid typo. +#define YYCC_DECL_MOVE(CLSNAME) \ + CLSNAME(CLSNAME&&) noexcept; \ + CLSNAME& operator=(CLSNAME&&) noexcept; + +/// @brief Make declaration of copy and move (\c constructor and \c operator\=) for given class to avoid typo. +#define YYCC_DECL_COPY_MOVE(CLSNAME) \ + YYCC_DECL_COPY(CLSNAME) \ + YYCC_DECL_MOVE(CLSNAME) + +/// @brief Make implementation signature of copy \c constrctor for given class and right operand name to avoid typo. +#define YYCC_IMPL_COPY_CTOR(CLSNAME, RHS) \ + CLSNAME::CLSNAME(const CLSNAME& RHS) + +/// @brief Make implementation signature of copy \c operator\= for given class and right operand name to avoid typo. +#define YYCC_IMPL_COPY_OPER(CLSNAME, RHS) \ + CLSNAME& CLSNAME::operator=(const CLSNAME& RHS) + +/// @brief Make implementation signature of move \c constrctor for given class and right operand name to avoid typo. +#define YYCC_IMPL_MOVE_CTOR(CLSNAME, RHS) \ + CLSNAME::CLSNAME(CLSNAME&& RHS) noexcept + +/// @brief Make implementation signature of move \c operator\= for given class and right operand name to avoid typo. +#define YYCC_IMPL_MOVE_OPER(CLSNAME, RHS) \ + CLSNAME& CLSNAME::operator=(CLSNAME&& RHS) noexcept diff --git a/src/yycc/windows/dialog.cpp b/src/yycc/windows/dialog.cpp index ea10029..7c17a84 100644 --- a/src/yycc/windows/dialog.cpp +++ b/src/yycc/windows/dialog.cpp @@ -1,344 +1,606 @@ #include "dialog.hpp" #if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) +#include "../string/op.hpp" +#include "../encoding/windows.hpp" +#include "../num/safe_cast.hpp" +#include "../num/safe_op.hpp" +#include + +#define ENC ::yycc::encoding::windows +#define OP ::yycc::string::op +#define SAFECAST ::yycc::num::safe_cast +#define SAFEOP ::yycc::num::safe_op +#define WINCOM ::yycc::windows::com + namespace yycc::windows::dialog { -#pragma region FileFilters +#pragma region WinFileFilters - bool FileFilters::Add(const yycc_char8_t* filter_name, std::initializer_list il) { - // assign filter name - if (filter_name == nullptr) return false; - FilterName name(filter_name); + WinFileFilters::WinFileFilters() : m_WinFilters(), m_WinDataStruct() {} - // assign filter patterns - FilterModes modes; - for (const yycc_char8_t* pattern : il) { - if (pattern != nullptr) modes.emplace_back(yycc_u8string(pattern)); - } + WinFileFilters::~WinFileFilters() {} - // check filter patterns - if (modes.empty()) return false; - - // add into pairs and return - m_Filters.emplace_back(std::make_pair(std::move(name), std::move(modes))); - return true; + YYCC_IMPL_COPY_CTOR(WinFileFilters, rhs) : m_WinFilters(rhs.m_WinFilters), m_WinDataStruct() { + // Update data + this->update(); } - bool FileFilters::Generate(WinFileFilters& win_result) const { - // clear Windows oriented data - win_result.Clear(); + YYCC_IMPL_COPY_OPER(WinFileFilters, rhs) { + // Copy data and mark desync + this->m_WinFilters = rhs.m_WinFilters; + this->m_WinDataStruct.clear(); + // Update data + this->update(); + // Return self + return *this; + } - // build new Windows oriented string vector first - for (const auto& it : m_Filters) { - // convert name to wchar - WinFileFilters::WinFilterName name; - if (!YYCC::EncodingHelper::UTF8ToWchar(it.first, name)) return false; + YYCC_IMPL_MOVE_CTOR(WinFileFilters, rhs) : m_WinFilters(std::move(rhs.m_WinFilters)), m_WinDataStruct() { + // In theory, there is no update should perform, + // however we do it because there is no guarantee that no memory allocation during this move. + this->update(); + } - // convert pattern and join them - const auto& filter_modes = it.second; - yycc_u8string joined_modes(YYCC::StringHelper::Join(filter_modes.begin(), filter_modes.end(), YYCC_U8(";"))); - WinFileFilters::WinFilterModes modes; - if (!YYCC::EncodingHelper::UTF8ToWchar(joined_modes, modes)) return false; + YYCC_IMPL_MOVE_OPER(WinFileFilters, rhs) { + // Move data + this->m_WinFilters = std::move(rhs.m_WinFilters); + this->m_WinDataStruct.clear(); + // Same reason for updating + this->update(); + // Return self + return *this; + } - // append new pair - win_result.m_WinFilters.emplace_back(std::make_pair(name, modes)); + UINT WinFileFilters::get_filter_count() const { + // We have check this length when building this class, + // so we can safely and directly cast it in there. + return static_cast(m_WinFilters.size()); + } + + const COMDLG_FILTERSPEC* WinFileFilters::get_filter_specs() const { + return m_WinDataStruct.data(); + } + + void WinFileFilters::update() { + // Make sure they have same size + auto n = m_WinFilters.size(); + m_WinDataStruct.resize(n); + + // Assign data one by one + for (auto i = 0; i < n; ++i) { + // Fetch item + const auto& filter = m_WinFilters[n]; + auto& data_struct = m_WinDataStruct[n]; + // Assign pointer + data_struct.pszName = filter.first.c_str(); + data_struct.pszSpec = filter.second.c_str(); } - - // check filter size - // if it overflow the maximum value, return false - size_t count = win_result.m_WinFilters.size(); - if (count > std::numeric_limits::max()) return false; - - // create new win data struct - // and assign string pointer from internal built win string vector. - win_result.m_WinDataStruct.reset(new COMDLG_FILTERSPEC[count]); - for (size_t i = 0u; i < count; ++i) { - win_result.m_WinDataStruct[i].pszName = win_result.m_WinFilters[i].first.c_str(); - win_result.m_WinDataStruct[i].pszSpec = win_result.m_WinFilters[i].second.c_str(); - } - - // everything is okey - return true; } #pragma endregion -#pragma region File Dialog +#pragma region FileFilters - bool FileDialog::Generate(WinFileDialog& win_result) const { - // clear Windows oriented data - win_result.Clear(); + FileFilters::FileFilters() : m_Filters() {} + + FileFilters::~FileFilters() {} + + void FileFilters::add_filter(const std::u8string_view& filter_name, std::initializer_list il) { + // assign filter name + FilterName name(filter_name); + // check filter name + if (name.empty()) { + throw std::invalid_argument("filter name is empty"); + } + + // assign filter patterns + FilterModes modes; + for (const auto& item : il) { + modes.emplace_back(item); + } + // check filter patterns + if (modes.empty()) { + throw std::invalid_argument("filter pattern list is empty"); + } + + // add into pairs and return + m_Filters.emplace_back(std::make_pair(std::move(name), std::move(modes))); + } + + size_t FileFilters::get_count() const { + return m_Filters.size(); + } + + void FileFilters::clear() { + m_Filters.clear(); + } + + DialogResult FileFilters::to_windows() const { + // prepare return value + WinFileFilters rv; + + // check filter size + // if it overflow the maximum value, return. + size_t count = m_Filters.size(); + if (!SAFECAST::try_to(count).has_value()) { + return std::unexpected(DialogError::TooManyFilters); + } + + // build new Windows oriented string vector + for (const auto& item : m_Filters) { + // convert name to wchar + auto win_name = ENC::to_wchar(item.first); + if (!win_name.has_value()) { + return std::unexpected(DialogError::BadEncoding); + } + + // join pattern string and convert to wchar + const auto& modes = item.second; + auto joined_modes = OP::join(modes.begin(), modes.end(), u8";"); + auto win_modes = ENC::to_wchar(joined_modes); + if (!win_modes.has_value()) { + return std::unexpected(DialogError::BadEncoding); + } + + // append this pair + rv.m_WinFilters.emplace_back(std::make_pair(win_name.value(), win_modes.value())); + } + + // update data struct + rv.update(); + + // okey, return value + return rv; + } + +#pragma endregion + +#pragma region WinFileDialog + + /** + * @brief Assistant function for making copy of IShellItem*. + * @param[in] obj The object for making copy. + * @return Copied COM object. + */ + static WINCOM::SmartIShellItem duplicate_shell_item(const WINCOM::SmartIShellItem& obj) { + // COM object is actually like a std::shared_ptr. + // So we simply copy its raw pointer and increase it reference counter if it is not nullptr. + WINCOM::SmartIShellItem rv(obj.get()); + if (rv) rv->AddRef(); + // Okey, return copied object. + return rv; + } + + WinFileDialog::WinFileDialog() : + m_WinOwner(NULL), m_WinFileTypes(), m_WinDefaultFileTypeIndex(0u), m_WinTitle(std::nullopt), m_WinInitFileName(std::nullopt), + m_WinInitDirectory(nullptr) {} + + WinFileDialog::~WinFileDialog() {} + + YYCC_IMPL_COPY_CTOR(WinFileDialog, rhs) : + m_WinOwner(rhs.m_WinOwner), m_WinFileTypes(rhs.m_WinFileTypes), m_WinDefaultFileTypeIndex(rhs.m_WinDefaultFileTypeIndex), + m_WinTitle(rhs.m_WinTitle), m_WinInitFileName(rhs.m_WinInitFileName), + m_WinInitDirectory(duplicate_shell_item(rhs.m_WinInitDirectory)) {} + + YYCC_IMPL_COPY_OPER(WinFileDialog, rhs) { + this->m_WinOwner = rhs.m_WinOwner; + this->m_WinFileTypes = rhs.m_WinFileTypes; + this->m_WinDefaultFileTypeIndex = rhs.m_WinDefaultFileTypeIndex; + this->m_WinTitle = rhs.m_WinTitle; + this->m_WinInitFileName = rhs.m_WinInitFileName; + this->m_WinInitDirectory = duplicate_shell_item(rhs.m_WinInitDirectory); + + return *this; + } + + bool WinFileDialog::has_owner() const { + return m_WinOwner != NULL; + } + + HWND WinFileDialog::get_owner() const { + if (!has_owner()) throw std::logic_error("fetch not set owner"); + return m_WinOwner; + } + + bool WinFileDialog::has_title() const { + return m_WinTitle.has_value(); + } + + const wchar_t* WinFileDialog::get_title() const { + if (!has_title()) throw std::logic_error("fetch not set title"); + return m_WinTitle.value().c_str(); + } + + bool WinFileDialog::has_init_file_name() const { + return m_WinInitFileName.has_value(); + } + + const wchar_t* WinFileDialog::get_init_file_name() const { + if (!has_init_file_name()) throw std::logic_error("fetch not set init file name"); + return m_WinInitFileName.value().c_str(); + } + + bool WinFileDialog::has_init_directory() const { + return m_WinInitDirectory.get() != nullptr; + } + + IShellItem* WinFileDialog::get_init_directory() const { + if (!has_init_file_name()) throw std::logic_error("fetch not set init directory"); + return m_WinInitDirectory.get(); + } + + const WinFileFilters& WinFileDialog::get_file_types() const { + return m_WinFileTypes; + } + + UINT WinFileDialog::get_default_file_type_index() const { + // This index has been checked so we return it directly. + return m_WinDefaultFileTypeIndex; + } + +#pragma endregion + +#pragma region FileDialog + + FileDialog::FileDialog() : + m_Owner(NULL), m_FileTypes(), m_DefaultFileTypeIndex(0u), m_Title(std::nullopt), m_InitFileName(std::nullopt), + m_InitDirectory(std::nullopt) {} + + FileDialog::~FileDialog() {} + + void FileDialog::set_owner(HWND owner) { + m_Owner = owner; + } + + void FileDialog::set_title(const std::u8string_view& title) { + m_Title = title; + } + + void FileDialog::unset_title() { + m_Title = std::nullopt; + } + + void FileDialog::set_init_file_name(const std::u8string_view& init_filename) { + m_InitFileName = init_filename; + } + + void FileDialog::unset_init_file_name() { + m_InitFileName = std::nullopt; + } + + void FileDialog::set_init_directory(const std::u8string_view& init_dir) { + m_InitDirectory = init_dir; + } + + void FileDialog::unset_init_directory() { + m_InitDirectory = std::nullopt; + } + + FileFilters& FileDialog::configre_file_types() { + return m_FileTypes; + } + + void FileDialog::set_default_file_type_index(size_t idx) { + m_DefaultFileTypeIndex = idx; + } + + void FileDialog::clear() { + m_Owner = NULL; + m_Title = std::nullopt; + m_InitFileName = std::nullopt; + m_InitDirectory = std::nullopt; + m_FileTypes.clear(); + m_DefaultFileTypeIndex = 0u; + } + + DialogResult FileDialog::to_windows() const { + // prepare return value + WinFileDialog rv; // set owner - win_result.m_WinOwner = m_Owner; - - // build file filters - if (!m_FileTypes.Generate(win_result.m_WinFileTypes)) return false; - - // check default file type index - // check value overflow (comparing with >= because we need plus 1 for file type index later) - if (m_DefaultFileTypeIndex >= std::numeric_limits::max()) return false; - // check invalid index (overflow the length or registered file types if there is file type) - if (m_FileTypes.Count() != 0u && m_DefaultFileTypeIndex >= m_FileTypes.Count()) return false; - // set index with additional plus according to Windows specification. - win_result.m_WinDefaultFileTypeIndex = static_cast(m_DefaultFileTypeIndex + 1); + rv.m_WinOwner = m_Owner; // build title and init file name - if (m_HasTitle) { - if (!YYCC::EncodingHelper::UTF8ToWchar(m_Title, win_result.m_WinTitle)) return false; - win_result.m_HasTitle = true; - } - if (m_HasInitFileName) { - if (!YYCC::EncodingHelper::UTF8ToWchar(m_InitFileName, win_result.m_WinInitFileName)) return false; - win_result.m_HasInitFileName = true; - } + if (m_Title.has_value()) { + auto win_title = ENC::to_wchar(m_Title.value()); + if (!win_title.has_value()) return std::unexpected(DialogError::BadEncoding); + else rv.m_WinTitle = win_title.value(); + } else rv.m_WinTitle = std::nullopt; + if (m_InitFileName.has_value()) { + auto win_init_file_name = ENC::to_wchar(m_InitFileName.value()); + if (!win_init_file_name.has_value()) return std::unexpected(DialogError::BadEncoding); + else rv.m_WinInitFileName = win_init_file_name.value(); + } else rv.m_WinInitFileName = std::nullopt; // fetch init directory - if (m_HasInitDirectory) { - // convert to wpath - std::wstring w_init_directory; - if (!YYCC::EncodingHelper::UTF8ToWchar(m_InitDirectory, w_init_directory)) return false; + if (m_InitDirectory.has_value()) { + // convert to wchar path + auto w_init_dir = ENC::to_wchar(m_InitDirectory.value()); + if (!w_init_dir.has_value()) { + return std::unexpected(DialogError::BadEncoding); + } // fetch IShellItem* // Ref: https://stackoverflow.com/questions/76306324/how-to-set-default-folder-for-ifileopendialog-interface IShellItem* init_directory = NULL; - HRESULT hr = SHCreateItemFromParsingName(w_init_directory.c_str(), NULL, IID_PPV_ARGS(&init_directory)); - if (FAILED(hr)) return false; + HRESULT hr = SHCreateItemFromParsingName(w_init_dir.value().c_str(), NULL, IID_PPV_ARGS(&init_directory)); + if (FAILED(hr)) return std::unexpected(DialogError::NoSuchDir); // assign IShellItem* - win_result.m_WinInitDirectory.reset(init_directory); + rv.m_WinInitDirectory.reset(init_directory); + } else rv.m_WinInitDirectory.reset(); + + // build file filters + auto win_file_types = m_FileTypes.to_windows(); + if (!win_file_types.has_value()) return std::unexpected(win_file_types.error()); + else rv.m_WinFileTypes = std::move(win_file_types.value()); + + // check and assign default file type index + // check whether it is out of filters range + if (m_DefaultFileTypeIndex >= m_FileTypes.get_count()) { + return std::unexpected(DialogError::IndexOutOfRange); } + // check whether it can be safely casted into UINT. + auto win_def_index_base0 = SAFECAST::try_to(m_DefaultFileTypeIndex); + if (!win_def_index_base0.has_value()) { + return std::unexpected(DialogError::IndexOverflow); + } + // check whether it can safely make addition with 1. + auto win_def_index = SAFEOP::checked_add(win_def_index_base0.value(), 1); + if (!win_def_index.has_value()) { + return std::unexpected(DialogError::IndexOverflow); + } + // okey, assign it + rv.m_WinDefaultFileTypeIndex = win_def_index.value(); // everything is okey - return true; + return rv; } #pragma endregion -#pragma region Windows Dialog Code +#pragma region Exposed Functions - enum class CommonFileDialogType { OpenFile, OpenMultipleFiles, SaveFile, OpenFolder }; + enum class GenericFileDialogType { OpenFile, OpenFiles, SaveFile, OpenFolder }; /** * @brief Extract display name from given IShellItem*. - * @param item[in] The pointer to IShellItem for extracting. - * @param ret[out] Extracted display name container. - * @return True if success, otherwise false. - * @remarks This is an assist function of CommonFileDialog. + * @param[in] item The pointer to IShellItem for extracting. + * @return Extract display name, or error occurs. + * @remarks This is an assist function of generic_file_dialog(). */ - static bool ExtractDisplayName(IShellItem* item, yycc_u8string& ret) { + static DialogResult extract_display_name(IShellItem* item) { // fetch display name from IShellItem* - LPWSTR _name; - HRESULT hr = item->GetDisplayName(SIGDN_FILESYSPATH, &_name); - if (FAILED(hr)) return false; - COMHelper::SmartLPWSTR display_name(_name); + LPWSTR display_name_ptr; + HRESULT hr = item->GetDisplayName(SIGDN_FILESYSPATH, &display_name_ptr); + if (FAILED(hr)) return std::unexpected(DialogError::BadCOMCall); + WINCOM::SmartLPWSTR display_name(display_name_ptr); - // convert result - if (!YYCC::EncodingHelper::WcharToUTF8(display_name.get(), ret)) return false; - - // finished - return true; + // convert result and return + return ENC::to_utf8(display_name.get()).transform_error([](auto err) { return DialogError::BadEncoding; }); } /** - * @brief General file dialog. - * @param params[in] User specified parameter controlling the behavior of this file dialog, - * including title, file types and etc. - * @param ret[out] The path to user selected files or folders. - * For multiple selection, the count of items >= 1. For other scenario, the count of item is 1. - * @return True if success, otherwise false (input parameters is wrong or user click "Cancel" in popup window). - * @remarks This function is the real underlying function of all dialog functions. + * @brief Generic file dialog. + * @details This function is the real underlying function of all dialog functions. + * @param[in] params User specified parameter controlling the behavior of this file dialog, including title, file types and etc. + * @return + * The full path to user selected files or folders. + * For multiple selection, the count of items >= 1. For others, the count of item must be 1. + * Or nothing (click "Cancel"), or error occurs. */ - template - static bool CommonFileDialog(const FileDialog& params, std::vector& ret) { + template + static DialogResult>> generic_file_dialog(const FileDialog& params) { // Reference: https://learn.microsoft.com/en-us/windows/win32/shell/common-file-dialog // prepare result variable HRESULT hr; + // create a const bad com call unexpected result + // because it is widely used in this function. + constexpr auto BAD_COM_CALL = std::unexpected(DialogError::BadCOMCall); + // check whether COM environment has been initialized - if (!COMHelper::IsInitialized()) return false; + if (!WINCOM::is_initialized()) return BAD_COM_CALL; // create file dialog instance // fetch dialog CLSID first CLSID dialog_clsid; switch (EDialogType) { - case CommonFileDialogType::OpenFile: - case CommonFileDialogType::OpenMultipleFiles: - case CommonFileDialogType::OpenFolder: + case GenericFileDialogType::OpenFile: + case GenericFileDialogType::OpenFiles: + case GenericFileDialogType::OpenFolder: dialog_clsid = CLSID_FileOpenDialog; break; - case CommonFileDialogType::SaveFile: + case GenericFileDialogType::SaveFile: dialog_clsid = CLSID_FileSaveDialog; break; default: - return false; + throw std::invalid_argument("unknown dialog type"); } // create raw dialog pointer - IFileDialog* _pfd = nullptr; - hr = CoCreateInstance(dialog_clsid, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&_pfd)); - if (FAILED(hr)) return false; + IFileDialog* raw_pfd = nullptr; + hr = CoCreateInstance(dialog_clsid, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&raw_pfd)); + if (FAILED(hr)) return BAD_COM_CALL; // create memory-safe dialog pointer - COMHelper::SmartIFileDialog pfd(_pfd); + WINCOM::SmartIFileDialog pfd(raw_pfd); // set options for dialog // before setting, always get the options first in order. // not to override existing options. DWORD dwFlags; hr = pfd->GetOptions(&dwFlags); - if (FAILED(hr)) return false; + if (FAILED(hr)) return BAD_COM_CALL; // modify options switch (EDialogType) { // We want user only can pick file system files: FOS_FORCEFILESYSTEM. // Open dialog default: FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR // Save dialog default: FOS_OVERWRITEPROMPT | FOS_NOREADONLYRETURN | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR // Pick folder: FOS_PICKFOLDERS - case CommonFileDialogType::OpenFile: + case GenericFileDialogType::OpenFile: dwFlags |= FOS_FORCEFILESYSTEM; dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; break; - case CommonFileDialogType::OpenMultipleFiles: + case GenericFileDialogType::OpenFiles: dwFlags |= FOS_FORCEFILESYSTEM; dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; dwFlags |= FOS_ALLOWMULTISELECT; break; - case CommonFileDialogType::SaveFile: + case GenericFileDialogType::SaveFile: dwFlags |= FOS_FORCEFILESYSTEM; dwFlags |= FOS_OVERWRITEPROMPT | FOS_NOREADONLYRETURN | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR; break; - case CommonFileDialogType::OpenFolder: + case GenericFileDialogType::OpenFolder: dwFlags |= FOS_FORCEFILESYSTEM; dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; dwFlags |= FOS_PICKFOLDERS; break; default: - return false; + throw std::invalid_argument("unknown dialog type"); } // set folder dialog options hr = pfd->SetOptions(dwFlags); - if (FAILED(hr)) return false; + if (FAILED(hr)) return BAD_COM_CALL; // build Windows used file dialog parameters - WinFileDialog win_params; - if (!params.Generate(win_params)) return false; + auto pending_win_params = params.to_windows(); + if (!pending_win_params.has_value()) return std::unexpected(pending_win_params.error()); + WinFileDialog win_params(std::move(pending_win_params.value())); // setup title and init file name - if (win_params.HasTitle()) { - hr = pfd->SetTitle(win_params.GetTitle()); - if (FAILED(hr)) return false; + if (win_params.has_title()) { + hr = pfd->SetTitle(win_params.get_title()); + if (FAILED(hr)) return BAD_COM_CALL; } - if (win_params.HasInitFileName()) { - hr = pfd->SetFileName(win_params.GetInitFileName()); - if (FAILED(hr)) return false; + if (win_params.has_init_file_name()) { + hr = pfd->SetFileName(win_params.get_init_file_name()); + if (FAILED(hr)) return BAD_COM_CALL; } // setup init directory - if (win_params.HasInitDirectory()) { - hr = pfd->SetFolder(win_params.GetInitDirectory()); + if (win_params.has_init_directory()) { + hr = pfd->SetFolder(win_params.get_init_directory()); + if (FAILED(hr)) return BAD_COM_CALL; } // set file types and default file index when we picking file - if constexpr (EDialogType != CommonFileDialogType::OpenFolder) { + if constexpr (EDialogType != GenericFileDialogType::OpenFolder) { // set file types list - const auto& file_filters = win_params.GetFileTypes(); - hr = pfd->SetFileTypes(file_filters.GetFilterCount(), file_filters.GetFilterSpecs()); - if (FAILED(hr)) return false; + const auto& file_filters = win_params.get_file_types(); + hr = pfd->SetFileTypes(file_filters.get_filter_count(), file_filters.get_filter_specs()); + if (FAILED(hr)) return BAD_COM_CALL; // set default file type index - hr = pfd->SetFileTypeIndex(win_params.GetDefaultFileTypeIndex()); - if (FAILED(hr)) return false; + hr = pfd->SetFileTypeIndex(win_params.get_default_file_type_index()); + if (FAILED(hr)) return BAD_COM_CALL; } - // show the dialog - hr = pfd->Show(win_params.HasOwner() ? win_params.GetOwner() : nullptr); - if (FAILED(hr)) return false; + // show the dialog and return if user click "Cancel" + hr = pfd->Show(win_params.has_owner() ? win_params.get_owner() : NULL); + if (FAILED(hr)) return std::nullopt; + // prepare return value + std::vector rv; // obtain result when user click "OK" button. switch (EDialogType) { - case CommonFileDialogType::OpenFile: - case CommonFileDialogType::OpenFolder: - case CommonFileDialogType::SaveFile: { + case GenericFileDialogType::OpenFile: + case GenericFileDialogType::OpenFolder: + case GenericFileDialogType::SaveFile: { // obtain one file entry - IShellItem* _item; - hr = pfd->GetResult(&_item); - if (FAILED(hr)) return false; - COMHelper::SmartIShellItem result_item(_item); + IShellItem* raw_item; + hr = pfd->GetResult(&raw_item); + if (FAILED(hr)) return BAD_COM_CALL; + WINCOM::SmartIShellItem item(raw_item); // extract display name - yycc_u8string result_name; - if (!ExtractDisplayName(result_item.get(), result_name)) return false; + auto display_name = extract_display_name(item.get()); + if (!display_name.has_value()) return std::unexpected(display_name.error()); // append result - ret.emplace_back(std::move(result_name)); + rv.emplace_back(std::move(display_name.value())); } break; - case CommonFileDialogType::OpenMultipleFiles: { + case GenericFileDialogType::OpenFiles: { // try casting file dialog to file open dialog // Ref: https://learn.microsoft.com/en-us/windows/win32/learnwin32/asking-an-object-for-an-interface - IFileOpenDialog* _pfod = nullptr; - hr = pfd->QueryInterface(IID_PPV_ARGS(&_pfod)); - if (FAILED(hr)) return false; - COMHelper::SmartIFileOpenDialog pfod(_pfod); + IFileOpenDialog* raw_pfod = nullptr; + hr = pfd->QueryInterface(IID_PPV_ARGS(&raw_pfod)); + if (FAILED(hr)) return BAD_COM_CALL; + WINCOM::SmartIFileOpenDialog pfod(raw_pfod); // obtain multiple file entires - IShellItemArray* _items; - hr = pfod->GetResults(&_items); - if (FAILED(hr)) return false; - COMHelper::SmartIShellItemArray result_items(_items); + IShellItemArray* raw_items; + hr = pfod->GetResults(&raw_items); + if (FAILED(hr)) return BAD_COM_CALL; + WINCOM::SmartIShellItemArray items(raw_items); // analyze file entries // get array count first - DWORD result_items_count = 0u; - hr = result_items->GetCount(&result_items_count); - if (FAILED(hr)) return false; + DWORD items_count = 0u; + hr = items->GetCount(&items_count); + if (FAILED(hr)) return BAD_COM_CALL; // iterate array - for (DWORD i = 0u; i < result_items_count; ++i) { + for (DWORD i = 0u; i < items_count; ++i) { // fetch item by index - IShellItem* _item; - ; - hr = result_items->GetItemAt(i, &_item); - if (FAILED(hr)) return false; - COMHelper::SmartIShellItem result_item(_item); + IShellItem* raw_item; + hr = items->GetItemAt(i, &raw_item); + if (FAILED(hr)) return BAD_COM_CALL; + WINCOM::SmartIShellItem item(raw_item); // extract display name - yycc_u8string result_name; - if (!ExtractDisplayName(result_item.get(), result_name)) return false; + auto display_name = extract_display_name(item.get()); + if (!display_name.has_value()) return std::unexpected(display_name.error()); // append result - ret.emplace_back(std::move(result_name)); + rv.emplace_back(std::move(display_name.value())); } } break; default: - return false; + throw std::invalid_argument("unknown dialog type"); } // everything is okey - return true; + return rv; } #pragma endregion #pragma region Wrapper Functions - bool OpenFileDialog(const FileDialog& params, yycc_u8string& ret) { - std::vector cache; - bool isok = CommonFileDialog(params, cache); - if (isok) ret = cache.front(); - return isok; - } - bool OpenMultipleFileDialog(const FileDialog& params, std::vector& ret) { - return CommonFileDialog(params, ret); - } - bool SaveFileDialog(const FileDialog& params, yycc_u8string& ret) { - std::vector cache; - bool isok = CommonFileDialog(params, cache); - if (isok) ret = cache.front(); - return isok; + /** + * @brief Assistant function for extracting item from given value returned by generic file dialog. + * @details This function will check whether inner vector only contain one item. + * @param[in] rhs The return value to be extracted. + * @return Extracted return value. + */ + static DialogResult> transform_generic_rv(DialogResult>>&& rhs) { + if (rhs.has_value()) { + const auto& inner = rhs.value(); + if (inner.has_value()) { + const auto& vec = inner.value(); + if (vec.size() != 1u) throw std::logic_error("return value doesn't contain exactly one item"); + else return std::move(vec.front()); + } else { + return std::nullopt; + } + } else { + return std::unexpected(rhs.error()); + } } - bool OpenFolderDialog(const FileDialog& params, yycc_u8string& ret) { - std::vector cache; - bool isok = CommonFileDialog(params, cache); - if (isok) ret = cache.front(); - return isok; + DialogResult> open_file(const FileDialog& params){ + return transform_generic_rv(generic_file_dialog(params)); + } + + DialogResult>> open_files(const FileDialog& params) { + return generic_file_dialog(params); + } + + DialogResult> save_file(const FileDialog& params) { + return transform_generic_rv(generic_file_dialog(params)); + } + + DialogResult> open_folder(const FileDialog& params) { + return transform_generic_rv(generic_file_dialog(params)); } #pragma endregion diff --git a/src/yycc/windows/dialog.hpp b/src/yycc/windows/dialog.hpp index e9a597c..f43cbe2 100644 --- a/src/yycc/windows/dialog.hpp +++ b/src/yycc/windows/dialog.hpp @@ -6,14 +6,19 @@ #include "../macro/class_copy_move.hpp" #include "com.hpp" #include +#include #include #include +#include +#include #include "import_guard_head.hpp" #include #include #include "import_guard_tail.hpp" +#define NS_YYCC_WINDOWS_COM ::yycc::windows::com + /** * @brief The namespace providing Windows universal dialog features. * @details @@ -22,39 +27,62 @@ */ namespace yycc::windows::dialog { + /// @brief The error occurs in this module. + enum class DialogError { + BadEncoding, ///< Error occurs when perform encoding convertion. + TooManyFilters, ///< The size of file filters list is too large for Windows. + IndexOverflow, ///< Default filter index is too large for Windows. + IndexOutOfRange, ///< Default filter index is out of range of filters list. + NoSuchDir, ///< Given initial directory path is invalid. + BadCOMCall, ///< Some COM function calls failed. + }; + + /// @brief The result type used in this module. + template + using DialogResult = std::expected; + /** * @private * @brief The class representing the file types region in file dialog. * @details - * This class is served for Windows used. + * This class is private and served for Windows used. * Programmer should \b not create this class manually. */ class WinFileFilters { friend class FileFilters; - friend class WinFileDialog; public: - WinFileFilters() : m_WinFilters(), m_WinDataStruct(nullptr) {} - YYCC_DELETE_COPY_MOVE(WinFileFilters) - - /// @brief Get the count of available file filters - UINT GetFilterCount() const { return static_cast(m_WinFilters.size()); } - /// @brief Get pointer to Windows used file filters declarations - const COMDLG_FILTERSPEC* GetFilterSpecs() const { return m_WinDataStruct.get(); } - - protected: using WinFilterModes = std::wstring; using WinFilterName = std::wstring; using WinFilterPair = std::pair; - std::vector m_WinFilters; - std::unique_ptr m_WinDataStruct; + public: + WinFileFilters(); + ~WinFileFilters(); + YYCC_DECL_COPY_MOVE(WinFileFilters) - /// @brief Clear all current file filters - void Clear() { - m_WinDataStruct.reset(); - m_WinFilters.clear(); - } + public: + /** + * @brief Get the count of available file filters + * @return Count of file filters. + */ + UINT get_filter_count() const; + /** + * @brief Get pointer to Windows used file filters declarations + * @return Pointer for Windows use. + */ + const COMDLG_FILTERSPEC* get_filter_specs() const; + + private: + /** + * @brief Update COMDLG_FILTERSPEC according to file filter list. + * @remarks Programmer \b MUST call this function after you modify m_WinFilters. + */ + void update(); + + private: + std::vector m_WinFilters; + std::vector m_WinDataStruct; }; /** @@ -62,52 +90,51 @@ namespace yycc::windows::dialog { * @details * This class is served for programmer using. * But you don't need create it on your own. - * You can simply fetch it by FileDialog::ConfigreFileTypes , + * You can simply fetch it by FileDialog::ConfigreFileTypes(), * because this class is a part of FileDialog. */ class FileFilters { public: - FileFilters() : m_Filters() {} - YYCC_DELETE_COPY_MOVE(FileFilters) + using FilterModes = std::vector; + using FilterName = std::u8string; + using FilterPair = std::pair; + public: + FileFilters(); + ~FileFilters(); + YYCC_DEFAULT_COPY_MOVE(FileFilters) + + public: /** * @brief Add a filter pair in file types list. * @param[in] filter_name The friendly name of the filter. * @param[in] il * A C++ initialize list containing acceptable file filter pattern. - * Every entries must be `const yycc_char8_t*` representing a single filter pattern. - * The list at least should have one valid pattern. - * This function will not validate these filter patterns, so please write them carefully. + * Every entries must be a string representing a single filter pattern. + * This list at least should have one pattern. * @return True if added success, otherwise false. - * @remarks - * This function allow you register multiple filter patterns for single friendly name. - * For example: Add(u8"Microsoft Word (*.doc; *.docx)", {u8"*.doc", u8"*.docx"}) + * @warning This function will not validate the content of these filter patterns, so please write them carefully. + * @exception std::invalid_argument Given filtern name is blank, or filter patterns is empty. */ - bool Add(const yycc_char8_t* filter_name, std::initializer_list il); + void add_filter(const std::u8string_view& filter_name, std::initializer_list il); /** - * @brief Get the count of added filter pairs. - * @return The count of already added filter pairs. - */ - size_t Count() const { return m_Filters.size(); } - - /// @brief Clear filter pairs for following re-use. - void Clear() { m_Filters.clear(); } + * @brief Get the count of added file filters. + * @return The count of added file filters. + */ + size_t get_count() const; + /** + * @brief Clear filter pairs for following re-use. + */ + void clear(); /** - * @brief Generate Windows dialog system used data struct. - * @param[out] win_result The class receiving the generated filter data struct. - * @return True if generation success, otherwise false. - * @remarks - * Programmer should not call this function, - * this function is used as YYCC internal code. - */ - bool Generate(WinFileFilters& win_result) const; - - protected: - using FilterModes = std::vector; - using FilterName = std::u8string; - using FilterPair = std::pair; + * @private + * @brief Build Windows used file filters struct. + * @return Built Windows used struct, or error occurs. + */ + DialogResult to_windows() const; + private: std::vector m_Filters; }; @@ -122,36 +149,33 @@ namespace yycc::windows::dialog { friend class FileDialog; public: - WinFileDialog() : - m_WinOwner(NULL), m_WinFileTypes(), m_WinDefaultFileTypeIndex(0u), m_HasTitle(false), m_HasInitFileName(false), m_WinTitle(), - m_WinInitFileName(), m_WinInitDirectory(nullptr) {} - YYCC_DELETE_COPY_MOVE(WinFileDialog) + WinFileDialog(); + ~WinFileDialog(); + YYCC_DECL_COPY(WinFileDialog) + YYCC_DEFAULT_MOVE(WinFileDialog) /// @brief Get whether this dialog has owner. - bool HasOwner() const { return m_WinOwner != NULL; } + bool has_owner() const; /// @brief Get the \c HWND of dialog owner. - HWND GetOwner() const { return m_WinOwner; } - - /// @brief Get the struct holding Windows used file filters data. - const WinFileFilters& GetFileTypes() const { return m_WinFileTypes; } - /// @brief Get the index of default selected file filter. - UINT GetDefaultFileTypeIndex() const { return m_WinDefaultFileTypeIndex; } - + HWND get_owner() const; /// @brief Get whether dialog has custom title. - bool HasTitle() const { return m_HasTitle; } + bool has_title() const; /// @brief Get custom title of dialog. - const wchar_t* GetTitle() const { return m_WinTitle.c_str(); } + const wchar_t* get_title() const; /// @brief Get whether dialog has custom initial file name. - bool HasInitFileName() const { return m_HasInitFileName; } + bool has_init_file_name() const; /// @brief Get custom initial file name of dialog - const wchar_t* GetInitFileName() const { return m_WinInitFileName.c_str(); } - + const wchar_t* get_init_file_name() const; /// @brief Get whether dialog has custom initial directory. - bool HasInitDirectory() const { return m_WinInitDirectory.get() != nullptr; } + bool has_init_directory() const; /// @brief Get custom initial directory of dialog. - IShellItem* GetInitDirectory() const { return m_WinInitDirectory.get(); } + IShellItem* get_init_directory() const; + /// @brief Get the struct holding Windows used file filters data. + const WinFileFilters& get_file_types() const; + /// @brief Get the index of default selected file filter. + UINT get_default_file_type_index() const; - protected: + private: HWND m_WinOwner; WinFileFilters m_WinFileTypes; /** @@ -162,20 +186,8 @@ namespace yycc::windows::dialog { * Because the same field located in FileDialog is 0-based index. */ UINT m_WinDefaultFileTypeIndex; - bool m_HasTitle, m_HasInitFileName; - std::wstring m_WinTitle, m_WinInitFileName; - com::SmartIShellItem m_WinInitDirectory; - - /// @brief Clear all data and reset them to default value. - void Clear() { - m_WinOwner = nullptr; - m_WinFileTypes.Clear(); - m_WinDefaultFileTypeIndex = 0u; - m_HasTitle = m_HasInitFileName = false; - m_WinTitle.clear(); - m_WinInitFileName.clear(); - m_WinInitDirectory.reset(); - } + std::optional m_WinTitle, m_WinInitFileName; + NS_YYCC_WINDOWS_COM::SmartIShellItem m_WinInitDirectory; }; /** @@ -186,79 +198,71 @@ namespace yycc::windows::dialog { */ class FileDialog { public: - FileDialog() : - m_Owner(NULL), m_FileTypes(), m_DefaultFileTypeIndex(0u), m_Title(), m_InitFileName(), m_InitDirectory(), m_HasTitle(false), - m_HasInitFileName(false), m_HasInitDirectory(false) {} - YYCC_DELETE_COPY_MOVE(FileDialog) + FileDialog(); + ~FileDialog(); + YYCC_DEFAULT_COPY_MOVE(FileDialog) /** * @brief Set the owner of dialog. * @param[in] owner The \c HWND pointing to the owner of dialog, or NULL to remove owner. */ - void SetOwner(HWND owner) { m_Owner = owner; } + void set_owner(HWND owner); /** * @brief Set custom title of dialog - * @param[in] title The string pointer to custom title, or nullptr to remove it. + * @param[in] title The string pointer to custom title. */ - void SetTitle(const yycc_char8_t* title) { - if (m_HasTitle = title != nullptr) m_Title = title; - } + void set_title(const std::u8string_view& title); + /** + * @brief Remove custom title of dialog (keep system default) + */ + void unset_title(); + /** + * @brief Set the initial file name of dialog + * @details If set, the file name will always be same one when opening dialog. + * @param[in] init_filename String pointer to initial file name. + */ + void set_init_file_name(const std::u8string_view& init_filename); + /** + * @brief Remove custom initial file name of dialog (keep system default) + */ + void unset_init_file_name(); + /** + * @brief Set the initial directory of dialog + * @details If set, the opended directory will always be the same one when opening dialog + * @param[in] init_dir String pointer to initial directory. + */ + void set_init_directory(const std::u8string_view& init_dir); + /** + * @brief Remove custom initial directory of dialog (keep system default) + */ + void unset_init_directory(); /** * @brief Fetch the struct describing file filters for future configuration. * @return The reference to the struct describing file filters. */ - FileFilters& ConfigreFileTypes() { return m_FileTypes; } + FileFilters& configre_file_types(); /** * @brief Set the index of default selected file filter. * @param[in] idx - * The index to default one. + * The index to default file filter. * This must be a valid index in file filters. */ - void SetDefaultFileTypeIndex(size_t idx) { m_DefaultFileTypeIndex = idx; } + void set_default_file_type_index(size_t idx); /** - * @brief Set the initial file name of dialog - * @details If set, the file name will always be same one when opening dialog. - * @param[in] init_filename String pointer to initial file name, or nullptr to remove it. - */ - void SetInitFileName(const yycc_char8_t* init_filename) { - if (m_HasInitFileName = init_filename != nullptr) m_InitFileName = init_filename; - } - /** - * @brief Set the initial directory of dialog - * @details If set, the opended directory will always be the same one when opening dialog - * @param[in] init_dir - * String pointer to initial directory. - * Invalid path or nullptr will remove this feature. - */ - void SetInitDirectory(const yycc_char8_t* init_dir) { - if (m_HasInitDirectory = init_dir != nullptr) m_InitDirectory = init_dir; - } - - /// @brief Clear file dialog parameters for following re-use. - void Clear() { - m_Owner = nullptr; - m_HasTitle = m_HasInitFileName = m_HasInitDirectory = false; - m_Title.clear(); - m_InitFileName.clear(); - m_InitDirectory.clear(); - m_FileTypes.Clear(); - m_DefaultFileTypeIndex = 0u; - } + * @brief Clear file dialog parameters for following re-use. + */ + void clear(); /** - * @brief Generate Windows dialog system used data struct. - * @param[out] win_result The class receiving the generated filter data struct. - * @return True if generation is success, otherwise false. - * @remarks - * Programmer should not call this function. - * This function is used as YYCC internal code. + * @private + * @brief Build Windows used dialog info struct. + * @return Built Windows used struct, or error occurs. */ - bool Generate(WinFileDialog& win_result) const; + DialogResult to_windows() const; protected: HWND m_Owner; - bool m_HasTitle, m_HasInitFileName, m_HasInitDirectory; - std::u8string m_Title, m_InitFileName, m_InitDirectory; + std::optional m_Title, m_InitFileName, m_InitDirectory; FileFilters m_FileTypes; /** * @brief The default selected file type in dialog @@ -270,35 +274,43 @@ namespace yycc::windows::dialog { size_t m_DefaultFileTypeIndex; }; + /** + * @brief The result after user close the dialog. + * @details + * It is an alias to \c std::optional. + * If it do not have value, it means that user click "Cancel" button. + * otherwise it contain a value that user selected in dialog. + */ + template + using DialogOutcome = std::optional; + /** * @brief Open the dialog which order user select single file to open. * @param[in] params The configuration of dialog. - * @param[out] ret Full path to user selected file. - * @return False if user calcel the operation or something went wrong, otherwise true. + * @return Full path to user selected file, or nothing (click "Cancel"), or error occurs. */ - bool OpenFileDialog(const FileDialog& params, std::u8string& ret); + DialogResult> open_file(const FileDialog& params); /** * @brief Open the dialog which order user select multiple file to open. * @param[in] params The configuration of dialog. - * @param[out] ret The list of full path of user selected files. - * @return False if user calcel the operation or something went wrong, otherwise true. + * @return The list of full path of user selected files, or nothing (click "Cancel"), or error occurs. */ - bool OpenMultipleFileDialog(const FileDialog& params, std::vector& ret); + DialogResult>> open_files(const FileDialog& params); /** * @brief Open the dialog which order user select single file to save. * @param[in] params The configuration of dialog. - * @param[out] ret Full path to user selected file. - * @return False if user calcel the operation or something went wrong, otherwise true. + * @return Full path to user selected file, or nothing (click "Cancel"), or error occurs. */ - bool SaveFileDialog(const FileDialog& params, std::u8string& ret); + DialogResult> save_file(const FileDialog& params); /** * @brief Open the dialog which order user select single directory to open. * @param[in] params The configuration of dialog. - * @param[out] ret Full path to user selected directory. - * @return False if user calcel the operation or something went wrong, otherwise true. + * @return Full path to user selected directory, or nothing (click "Cancel"), or error occurs. */ - bool OpenFolderDialog(const FileDialog& params, std::u8string& ret); + DialogResult> open_folder(const FileDialog& params); } +#undef NS_YYCC_WINDOWS_COM + #endif