From 580b096cb377b9b0e62d0d6ac82ce0e7bfb3dda8 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 21 Aug 2025 11:00:04 +0800 Subject: [PATCH] fix: fix bug for windows dialog --- src/YYCCLegacy/DialogHelper.cpp | 378 ------------------------------ src/YYCCLegacy/DialogHelper.hpp | 312 ------------------------ src/yycc/windows/dialog.cpp | 30 ++- src/yycc/windows/dialog.hpp | 1 + testbench/CMakeLists.txt | 2 +- testbench/main_legacy.cpp | 206 ---------------- testbench/yycc/windows/dialog.cpp | 45 +++- 7 files changed, 64 insertions(+), 910 deletions(-) delete mode 100644 src/YYCCLegacy/DialogHelper.cpp delete mode 100644 src/YYCCLegacy/DialogHelper.hpp diff --git a/src/YYCCLegacy/DialogHelper.cpp b/src/YYCCLegacy/DialogHelper.cpp deleted file mode 100644 index 6e4e7d1..0000000 --- a/src/YYCCLegacy/DialogHelper.cpp +++ /dev/null @@ -1,378 +0,0 @@ -#include "DialogHelper.hpp" -#if defined(YYCC_OS_WINDOWS) - -#include "EncodingHelper.hpp" -#include "StringHelper.hpp" - -namespace YYCC::DialogHelper { - -#pragma region FileFilters - - 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); - - // assign filter patterns - FilterModes modes; - for (const yycc_char8_t* pattern : il) { - if (pattern != nullptr) - modes.emplace_back(yycc_u8string(pattern)); - } - - // 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; - } - - bool FileFilters::Generate(WinFileFilters& win_result) const { - // clear Windows oriented data - win_result.Clear(); - - // 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; - - // 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; - - // append new pair - win_result.m_WinFilters.emplace_back(std::make_pair(name, modes)); - } - - // 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 - - bool FileDialog::Generate(WinFileDialog& win_result) const { - // clear Windows oriented data - win_result.Clear(); - - // 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); - - // 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; - } - - // 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; - - // 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; - - // assign IShellItem* - win_result.m_WinInitDirectory.reset(init_directory); - } - - // everything is okey - return true; - } - -#pragma endregion - -#pragma region Windows Dialog Code - - enum class CommonFileDialogType { - OpenFile, - OpenMultipleFiles, - 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. - */ - static bool ExtractDisplayName(IShellItem* item, yycc_u8string& ret) { - // fetch display name from IShellItem* - LPWSTR _name; - HRESULT hr = item->GetDisplayName(SIGDN_FILESYSPATH, &_name); - if (FAILED(hr)) return false; - COMHelper::SmartLPWSTR display_name(_name); - - // convert result - if (!YYCC::EncodingHelper::WcharToUTF8(display_name.get(), ret)) - return false; - - // finished - return true; - } - - /** - * @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. - */ - template - static bool CommonFileDialog(const FileDialog& params, std::vector& ret) { - // Reference: https://learn.microsoft.com/en-us/windows/win32/shell/common-file-dialog - // prepare result variable - HRESULT hr; - - // check whether COM environment has been initialized - if (!COMHelper::IsInitialized()) return false; - - // create file dialog instance - // fetch dialog CLSID first - CLSID dialog_clsid; - switch (EDialogType) { - case CommonFileDialogType::OpenFile: - case CommonFileDialogType::OpenMultipleFiles: - case CommonFileDialogType::OpenFolder: - dialog_clsid = CLSID_FileOpenDialog; - break; - case CommonFileDialogType::SaveFile: - dialog_clsid = CLSID_FileSaveDialog; - break; - default: - return false; - } - // create raw dialog pointer - IFileDialog* _pfd = nullptr; - hr = CoCreateInstance( - dialog_clsid, - NULL, - CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&_pfd) - ); - if (FAILED(hr)) return false; - // create memory-safe dialog pointer - COMHelper::SmartIFileDialog pfd(_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; - // 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: - dwFlags |= FOS_FORCEFILESYSTEM; - dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; - break; - case CommonFileDialogType::OpenMultipleFiles: - dwFlags |= FOS_FORCEFILESYSTEM; - dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; - dwFlags |= FOS_ALLOWMULTISELECT; - break; - case CommonFileDialogType::SaveFile: - dwFlags |= FOS_FORCEFILESYSTEM; - dwFlags |= FOS_OVERWRITEPROMPT | FOS_NOREADONLYRETURN | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR; - break; - case CommonFileDialogType::OpenFolder: - dwFlags |= FOS_FORCEFILESYSTEM; - dwFlags |= FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; - dwFlags |= FOS_PICKFOLDERS; - break; - default: - return false; - } - // set folder dialog options - hr = pfd->SetOptions(dwFlags); - if (FAILED(hr)) return false; - - // build Windows used file dialog parameters - WinFileDialog win_params; - if (!params.Generate(win_params)) - return false; - - // setup title and init file name - if (win_params.HasTitle()) { - hr = pfd->SetTitle(win_params.GetTitle()); - if (FAILED(hr)) return false; - } - if (win_params.HasInitFileName()) { - hr = pfd->SetFileName(win_params.GetInitFileName()); - if (FAILED(hr)) return false; - } - - // setup init directory - if (win_params.HasInitDirectory()) { - hr = pfd->SetFolder(win_params.GetInitDirectory()); - } - - // set file types and default file index when we picking file - if constexpr (EDialogType != CommonFileDialogType::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; - - // set default file type index - hr = pfd->SetFileTypeIndex(win_params.GetDefaultFileTypeIndex()); - if (FAILED(hr)) return false; - } - - // show the dialog - hr = pfd->Show(win_params.HasOwner() ? win_params.GetOwner() : nullptr); - if (FAILED(hr)) return false; - - // obtain result when user click "OK" button. - switch (EDialogType) { - case CommonFileDialogType::OpenFile: - case CommonFileDialogType::OpenFolder: - case CommonFileDialogType::SaveFile: - { - // obtain one file entry - IShellItem* _item; - hr = pfd->GetResult(&_item); - if (FAILED(hr)) return false; - COMHelper::SmartIShellItem result_item(_item); - - // extract display name - yycc_u8string result_name; - if (!ExtractDisplayName(result_item.get(), result_name)) - return false; - - // append result - ret.emplace_back(std::move(result_name)); - } - break; - case CommonFileDialogType::OpenMultipleFiles: - { - // 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); - - // obtain multiple file entires - IShellItemArray* _items; - hr = pfod->GetResults(&_items); - if (FAILED(hr)) return false; - COMHelper::SmartIShellItemArray result_items(_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; - // iterate array - for (DWORD i = 0u; i < result_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); - - // extract display name - yycc_u8string result_name; - if (!ExtractDisplayName(result_item.get(), result_name)) - return false; - - // append result - ret.emplace_back(std::move(result_name)); - } - } - break; - default: - return false; - } - - // everything is okey - return true; - } - -#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; - } - - bool OpenFolderDialog(const FileDialog& params, yycc_u8string& ret) { - std::vector cache; - bool isok = CommonFileDialog(params, cache); - if (isok) ret = cache.front(); - return isok; - } - -#pragma endregion - -} - -#endif diff --git a/src/YYCCLegacy/DialogHelper.hpp b/src/YYCCLegacy/DialogHelper.hpp deleted file mode 100644 index 01af5c8..0000000 --- a/src/YYCCLegacy/DialogHelper.hpp +++ /dev/null @@ -1,312 +0,0 @@ -#pragma once -#include "YYCCInternal.hpp" -#if defined(YYCC_OS_WINDOWS) - -#include "COMHelper.hpp" -#include -#include -#include - -#include "WinImportPrefix.hpp" -#include -#include -#include "WinImportSuffix.hpp" - -/** - * @brief The namespace providing Windows universal dialog features. - * @details - * This namespace only available on Windows platform. - * See also \ref dialog_helper. -*/ -namespace YYCC::DialogHelper { - - /** - * @brief The class representing the file types region in file dialog. - * @details - * This class is 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_DEL_CLS_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; - - /// @brief Clear all current file filters - void Clear() { - m_WinDataStruct.reset(); - m_WinFilters.clear(); - } - }; - - /** - * @brief The class representing the file types region in file 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 , - * because this class is a part of FileDialog. - */ - class FileFilters { - public: - FileFilters() : m_Filters() {} - YYCC_DEL_CLS_COPY_MOVE(FileFilters); - - /** - * @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. - * @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"}) - */ - bool Add(const yycc_char8_t* 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 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 = yycc_u8string; - using FilterPair = std::pair; - - std::vector m_Filters; - }; - - /** - * @brief The class representing the file dialog. - * @details - * This class is served for Windows used. - * Programmer should \b not create this class manually. - */ - class WinFileDialog { - 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_DEL_CLS_COPY_MOVE(WinFileDialog); - - /// @brief Get whether this dialog has owner. - bool HasOwner() const { return m_WinOwner != NULL; } - /// @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; } - - /// @brief Get whether dialog has custom title. - bool HasTitle() const { return m_HasTitle; } - /// @brief Get custom title of dialog. - const wchar_t* GetTitle() const { return m_WinTitle.c_str(); } - /// @brief Get whether dialog has custom initial file name. - bool HasInitFileName() const { return m_HasInitFileName; } - /// @brief Get custom initial file name of dialog - const wchar_t* GetInitFileName() const { return m_WinInitFileName.c_str(); } - - /// @brief Get whether dialog has custom initial directory. - bool HasInitDirectory() const { return m_WinInitDirectory.get() != nullptr; } - /// @brief Get custom initial directory of dialog. - IShellItem* GetInitDirectory() const { return m_WinInitDirectory.get(); } - - protected: - HWND m_WinOwner; - WinFileFilters m_WinFileTypes; - /** - * @brief The default selected file type in dialog - * @remarks - * This is 1-based index according to Windows specification. - * In other words, when generating this struct from FileDialog to this struct this field should plus 1. - * 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; - COMHelper::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(); - } - }; - - /** - * @brief The class representing the file dialog. - * @details - * This class is served for programming using to describe every aspectes of the dialog. - * For how to use this struct, see \ref dialog_helper. - */ - 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_DEL_CLS_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; } - /** - * @brief Set custom title of dialog - * @param[in] title The string pointer to custom title, or nullptr to remove it. - */ - void SetTitle(const yycc_char8_t* title) { - if (m_HasTitle = title != nullptr) - m_Title = title; - } - /** - * @brief Fetch the struct describing file filters for future configuration. - * @return The reference to the struct describing file filters. - */ - FileFilters& ConfigreFileTypes() { - return m_FileTypes; - } - /** - * @brief Set the index of default selected file filter. - * @param[in] idx - * The index to default one. - * This must be a valid index in file filters. - */ - void SetDefaultFileTypeIndex(size_t idx) { m_DefaultFileTypeIndex = 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 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. - */ - bool Generate(WinFileDialog& win_result) const; - - protected: - HWND m_Owner; - bool m_HasTitle, m_HasInitFileName, m_HasInitDirectory; - yycc_u8string m_Title, m_InitFileName, m_InitDirectory; - FileFilters m_FileTypes; - /** - * @brief The default selected file type in dialog - * @remarks - * The index Windows used is 1-based index. - * But for universal experience, we order this is 0-based index. - * And do convertion when generating Windows used struct. - */ - size_t m_DefaultFileTypeIndex; - }; - - /** - * @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. - */ - bool OpenFileDialog(const FileDialog& params, yycc_u8string& ret); - /** - * @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. - */ - bool OpenMultipleFileDialog(const FileDialog& params, std::vector& ret); - /** - * @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. - */ - bool SaveFileDialog(const FileDialog& params, yycc_u8string& ret); - /** - * @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. - */ - bool OpenFolderDialog(const FileDialog& params, yycc_u8string& ret); - -} - -#endif diff --git a/src/yycc/windows/dialog.cpp b/src/yycc/windows/dialog.cpp index 7c17a84..c8fa924 100644 --- a/src/yycc/windows/dialog.cpp +++ b/src/yycc/windows/dialog.cpp @@ -70,8 +70,8 @@ namespace yycc::windows::dialog { // 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]; + const auto& filter = m_WinFilters[i]; + auto& data_struct = m_WinDataStruct[i]; // Assign pointer data_struct.pszName = filter.first.c_str(); data_struct.pszSpec = filter.second.c_str(); @@ -151,6 +151,8 @@ namespace yycc::windows::dialog { rv.update(); // okey, return value + // please note that we do not check whether this list is empty in there. + // this was done in generic_file_dialog() function. return rv; } @@ -337,10 +339,6 @@ namespace yycc::windows::dialog { 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()) { @@ -351,7 +349,9 @@ namespace yycc::windows::dialog { if (!win_def_index.has_value()) { return std::unexpected(DialogError::IndexOverflow); } - // okey, assign it + // okey, assign it. + // please note we do not check the relation between this variable and file filters. + // this was done in generic_file_dialog() function. rv.m_WinDefaultFileTypeIndex = win_def_index.value(); // everything is okey @@ -485,11 +485,21 @@ namespace yycc::windows::dialog { // set file types and default file index when we picking file if constexpr (EDialogType != GenericFileDialogType::OpenFolder) { - // set file types list + // fetch file filters const auto& file_filters = win_params.get_file_types(); + + // first, check whether file filters is empty + if (file_filters.get_filter_count() == 0) { + return std::unexpected(DialogError::EmptyFilters); + } + // then validate index + if (win_params.get_default_file_type_index() > file_filters.get_filter_count()) { + return std::unexpected(DialogError::IndexOutOfRange); + } + + // set file types list 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.get_default_file_type_index()); if (FAILED(hr)) return BAD_COM_CALL; @@ -578,7 +588,7 @@ namespace yycc::windows::dialog { 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 vec.front(); } else { return std::nullopt; } diff --git a/src/yycc/windows/dialog.hpp b/src/yycc/windows/dialog.hpp index f43cbe2..c2be9de 100644 --- a/src/yycc/windows/dialog.hpp +++ b/src/yycc/windows/dialog.hpp @@ -32,6 +32,7 @@ namespace yycc::windows::dialog { 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. + EmptyFilters, ///< File filters is empty when picking file. IndexOutOfRange, ///< Default filter index is out of range of filters list. NoSuchDir, ///< Given initial directory path is invalid. BadCOMCall, ///< Some COM function calls failed. diff --git a/testbench/CMakeLists.txt b/testbench/CMakeLists.txt index fa797eb..bad5efb 100644 --- a/testbench/CMakeLists.txt +++ b/testbench/CMakeLists.txt @@ -30,7 +30,7 @@ PRIVATE yycc/encoding/windows.cpp yycc/encoding/iconv.cpp yycc/windows/com.cpp - #yycc/windows/dialog.cpp + yycc/windows/dialog.cpp yycc/windows/winfct.cpp yycc/carton/pycodec.cpp diff --git a/testbench/main_legacy.cpp b/testbench/main_legacy.cpp index c5ed363..7c5c780 100644 --- a/testbench/main_legacy.cpp +++ b/testbench/main_legacy.cpp @@ -7,94 +7,6 @@ namespace Console = YYCC::ConsoleHelper; namespace YYCCTestbench { -#pragma region Unicode Test Data - - // UNICODE Test Strings - // Ref: https://stackoverflow.com/questions/478201/how-to-test-an-application-for-correct-encoding-e-g-utf-8 -#define TEST_UNICODE_STR_JAPAN "\u30E6\u30FC\u30B6\u30FC\u5225\u30B5\u30A4\u30C8" -#define TEST_UNICODE_STR_CHINA "\u7B80\u4F53\u4E2D\u6587" -#define TEST_UNICODE_STR_KOREA "\uD06C\uB85C\uC2A4 \uD50C\uB7AB\uD3FC\uC73C\uB85C" -#define TEST_UNICODE_STR_ISRAEL "\u05DE\u05D3\u05D5\u05E8\u05D9\u05DD \u05DE\u05D1\u05D5\u05E7\u05E9\u05D9\u05DD" -#define TEST_UNICODE_STR_EGYPT "\u0623\u0641\u0636\u0644 \u0627\u0644\u0628\u062D\u0648\u062B" -#define TEST_UNICODE_STR_GREECE "\u03A3\u1F72 \u03B3\u03BD\u03C9\u03C1\u03AF\u03B6\u03C9 \u1F00\u03C0\u1F78" -#define TEST_UNICODE_STR_RUSSIA "\u0414\u0435\u0441\u044F\u0442\u0443\u044E \u041C\u0435\u0436\u0434\u0443\u043D\u0430\u0440\u043E\u0434\u043D\u0443\u044E" -#define TEST_UNICODE_STR_THAILAND "\u0E41\u0E1C\u0E48\u0E19\u0E14\u0E34\u0E19\u0E2E\u0E31\u0E48\u0E19\u0E40\u0E2A\u0E37\u0E48\u0E2D\u0E21\u0E42\u0E17\u0E23\u0E21\u0E41\u0E2A\u0E19\u0E2A\u0E31\u0E07\u0E40\u0E27\u0E0A" -#define TEST_UNICODE_STR_FRANCE "fran\u00E7ais langue \u00E9trang\u00E8re" -#define TEST_UNICODE_STR_SPAIN "ma\u00F1ana ol\u00E9" -#define TEST_UNICODE_STR_MATHMATICS "\u222E E\u22C5da = Q, n \u2192 \u221E, \u2211 f(i) = \u220F g(i)" -#define TEST_UNICODE_STR_EMOJI "\U0001F363 \u2716 \U0001F37A" // sushi x beer mug - -#define CONCAT(prefix, strl) prefix ## strl -#define CPP_U8_LITERAL(strl) YYCC_U8(strl) -#define CPP_U16_LITERAL(strl) CONCAT(u, strl) -#define CPP_U32_LITERAL(strl) CONCAT(U, strl) -#define CPP_WSTR_LITERAL(strl) CONCAT(L, strl) - - static std::vector c_UTF8TestStrTable { - CPP_U8_LITERAL(TEST_UNICODE_STR_JAPAN), - CPP_U8_LITERAL(TEST_UNICODE_STR_CHINA), - CPP_U8_LITERAL(TEST_UNICODE_STR_KOREA), - CPP_U8_LITERAL(TEST_UNICODE_STR_ISRAEL), - CPP_U8_LITERAL(TEST_UNICODE_STR_EGYPT), - CPP_U8_LITERAL(TEST_UNICODE_STR_GREECE), - CPP_U8_LITERAL(TEST_UNICODE_STR_RUSSIA), - CPP_U8_LITERAL(TEST_UNICODE_STR_THAILAND), - CPP_U8_LITERAL(TEST_UNICODE_STR_FRANCE), - CPP_U8_LITERAL(TEST_UNICODE_STR_SPAIN), - CPP_U8_LITERAL(TEST_UNICODE_STR_MATHMATICS), - CPP_U8_LITERAL(TEST_UNICODE_STR_EMOJI), - }; - static std::vector c_WStrTestStrTable { - CPP_WSTR_LITERAL(TEST_UNICODE_STR_JAPAN), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_CHINA), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_KOREA), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_ISRAEL), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_EGYPT), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_GREECE), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_RUSSIA), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_THAILAND), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_FRANCE), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_SPAIN), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_MATHMATICS), - CPP_WSTR_LITERAL(TEST_UNICODE_STR_EMOJI), - }; - static std::vector c_UTF16TestStrTable { - CPP_U16_LITERAL(TEST_UNICODE_STR_JAPAN), - CPP_U16_LITERAL(TEST_UNICODE_STR_CHINA), - CPP_U16_LITERAL(TEST_UNICODE_STR_KOREA), - CPP_U16_LITERAL(TEST_UNICODE_STR_ISRAEL), - CPP_U16_LITERAL(TEST_UNICODE_STR_EGYPT), - CPP_U16_LITERAL(TEST_UNICODE_STR_GREECE), - CPP_U16_LITERAL(TEST_UNICODE_STR_RUSSIA), - CPP_U16_LITERAL(TEST_UNICODE_STR_THAILAND), - CPP_U16_LITERAL(TEST_UNICODE_STR_FRANCE), - CPP_U16_LITERAL(TEST_UNICODE_STR_SPAIN), - CPP_U16_LITERAL(TEST_UNICODE_STR_MATHMATICS), - CPP_U16_LITERAL(TEST_UNICODE_STR_EMOJI), - }; - static std::vector c_UTF32TestStrTable { - CPP_U32_LITERAL(TEST_UNICODE_STR_JAPAN), - CPP_U32_LITERAL(TEST_UNICODE_STR_CHINA), - CPP_U32_LITERAL(TEST_UNICODE_STR_KOREA), - CPP_U32_LITERAL(TEST_UNICODE_STR_ISRAEL), - CPP_U32_LITERAL(TEST_UNICODE_STR_EGYPT), - CPP_U32_LITERAL(TEST_UNICODE_STR_GREECE), - CPP_U32_LITERAL(TEST_UNICODE_STR_RUSSIA), - CPP_U32_LITERAL(TEST_UNICODE_STR_THAILAND), - CPP_U32_LITERAL(TEST_UNICODE_STR_FRANCE), - CPP_U32_LITERAL(TEST_UNICODE_STR_SPAIN), - CPP_U32_LITERAL(TEST_UNICODE_STR_MATHMATICS), - CPP_U32_LITERAL(TEST_UNICODE_STR_EMOJI), - }; - -#undef CPP_WSTR_LITERAL -#undef CPP_U32_LITERAL -#undef CPP_U16_LITERAL -#undef CPP_U8_LITERAL -#undef CONCAT - -#pragma endregion - static void Assert(bool condition, const YYCC::yycc_char8_t* description) { if (condition) { Console::FormatLine(YYCC_U8(YYCC_COLOR_LIGHT_GREEN("OK: %s")), description); @@ -105,24 +17,6 @@ namespace YYCCTestbench { } static void ConsoleTestbench() { - // Color Test - Console::EnableColorfulConsole(); - Console::WriteLine(YYCC_U8("Color Test:")); - -#define TEST_MACRO(col) Console::WriteLine(YYCC_U8("\t" YYCC_COLOR_ ## col ("\u2588\u2588") YYCC_COLOR_LIGHT_ ## col("\u2588\u2588") " " #col " / LIGHT " #col )); - // U+2588 is full block - - TEST_MACRO(BLACK); - TEST_MACRO(RED); - TEST_MACRO(GREEN); - TEST_MACRO(YELLOW); - TEST_MACRO(BLUE); - TEST_MACRO(MAGENTA); - TEST_MACRO(CYAN); - TEST_MACRO(WHITE); - -#undef TEST_MACRO - // UTF8 Output Test Console::WriteLine(YYCC_U8("UTF8 Output Test:")); for (const auto& strl : c_UTF8TestStrTable) { @@ -142,40 +36,6 @@ namespace YYCCTestbench { } - static void DialogTestbench() { -#if defined(YYCC_OS_WINDOWS) - - YYCC::yycc_u8string ret; - std::vector rets; - - YYCC::DialogHelper::FileDialog params; - auto& filters = params.ConfigreFileTypes(); - filters.Add(YYCC_U8("Microsoft Word (*.docx; *.doc)"), { YYCC_U8("*.docx"), YYCC_U8("*.doc") }); - filters.Add(YYCC_U8("Microsoft Excel (*.xlsx; *.xls)"), { YYCC_U8("*.xlsx"), YYCC_U8("*.xls") }); - filters.Add(YYCC_U8("Microsoft PowerPoint (*.pptx; *.ppt)"), { YYCC_U8("*.pptx"), YYCC_U8("*.ppt") }); - filters.Add(YYCC_U8("Text File (*.txt)"), { YYCC_U8("*.txt") }); - filters.Add(YYCC_U8("All Files (*.*)"), { YYCC_U8("*.*") }); - params.SetDefaultFileTypeIndex(0u); - if (YYCC::DialogHelper::OpenFileDialog(params, ret)) { - Console::FormatLine(YYCC_U8("Open File: %s"), ret.c_str()); - } - if (YYCC::DialogHelper::OpenMultipleFileDialog(params, rets)) { - Console::WriteLine(YYCC_U8("Open Multiple Files:")); - for (const auto& item : rets) { - Console::FormatLine(YYCC_U8("\t%s"), item.c_str()); - } - } - if (YYCC::DialogHelper::SaveFileDialog(params, ret)) { - Console::FormatLine(YYCC_U8("Save File: %s"), ret.c_str()); - } - params.Clear(); - if (YYCC::DialogHelper::OpenFolderDialog(params, ret)) { - Console::FormatLine(YYCC_U8("Open Folder: %s"), ret.c_str()); - } - -#endif - } - static void ExceptionTestbench() { #if defined(YYCC_OS_WINDOWS) @@ -210,72 +70,6 @@ namespace YYCCTestbench { #endif } - static void WinFctTestbench() { -#if defined(YYCC_OS_WINDOWS) - - HMODULE test_current_module; - Assert((test_current_module = YYCC::WinFctHelper::GetCurrentModule()) != nullptr, YYCC_U8("YYCC::WinFctHelper::GetCurrentModule")); - Console::FormatLine(YYCC_U8("Current Module HANDLE: 0x%" PRI_XPTR_LEFT_PADDING PRIXPTR), test_current_module); - - YYCC::yycc_u8string test_temp; - Assert(YYCC::WinFctHelper::GetTempDirectory(test_temp), YYCC_U8("YYCC::WinFctHelper::GetTempDirectory")); - Console::FormatLine(YYCC_U8("Temp Directory: %s"), test_temp.c_str()); - - YYCC::yycc_u8string test_module_name; - Assert(YYCC::WinFctHelper::GetModuleFileName(YYCC::WinFctHelper::GetCurrentModule(), test_module_name), YYCC_U8("YYCC::WinFctHelper::GetModuleFileName")); - Console::FormatLine(YYCC_U8("Current Module File Name: %s"), test_module_name.c_str()); - - YYCC::yycc_u8string test_localappdata_path; - Assert(YYCC::WinFctHelper::GetLocalAppData(test_localappdata_path), YYCC_U8("YYCC::WinFctHelper::GetLocalAppData")); - Console::FormatLine(YYCC_U8("Local AppData: %s"), test_localappdata_path.c_str()); - - Assert(YYCC::WinFctHelper::IsValidCodePage(static_cast(1252)), YYCC_U8("YYCC::WinFctHelper::IsValidCodePage")); - Assert(!YYCC::WinFctHelper::IsValidCodePage(static_cast(114514)), YYCC_U8("YYCC::WinFctHelper::IsValidCodePage")); - - // MARK: There is no testbench for MoveFile, CopyFile DeleteFile. - // Because they can operate file system files. - // And may cause test environment entering unstable status. - -#endif - } - - static void StdPatchTestbench() { - - // Std Path - - std::filesystem::path test_path; - for (const auto& strl : c_UTF8TestStrTable) { - test_path /= YYCC::StdPatch::ToStdPath(strl); - } - YYCC::yycc_u8string test_slashed_path(YYCC::StdPatch::ToUTF8Path(test_path)); - -#if defined(YYCC_OS_WINDOWS) - std::wstring wdelimiter(1u, std::filesystem::path::preferred_separator); - YYCC::yycc_u8string delimiter(YYCC::EncodingHelper::WcharToUTF8(wdelimiter)); -#else - YYCC::yycc_u8string delimiter(1u, std::filesystem::path::preferred_separator); -#endif - YYCC::yycc_u8string test_joined_path(YYCC::StringHelper::Join(c_UTF8TestStrTable.begin(), c_UTF8TestStrTable.end(), delimiter)); - - Assert(test_slashed_path == test_joined_path, YYCC_U8("YYCC::StdPatch::ToStdPath, YYCC::StdPatch::ToUTF8Path")); - - // StartsWith, EndsWith - YYCC::yycc_u8string test_starts_ends_with(YYCC_U8("aaabbbccc")); - Assert(YYCC::StdPatch::StartsWith(test_starts_ends_with, YYCC_U8("aaa")), YYCC_U8("YYCC::StdPatch::StartsWith")); - Assert(!YYCC::StdPatch::StartsWith(test_starts_ends_with, YYCC_U8("ccc")), YYCC_U8("YYCC::StdPatch::StartsWith")); - Assert(!YYCC::StdPatch::EndsWith(test_starts_ends_with, YYCC_U8("aaa")), YYCC_U8("YYCC::StdPatch::EndsWith")); - Assert(YYCC::StdPatch::EndsWith(test_starts_ends_with, YYCC_U8("ccc")), YYCC_U8("YYCC::StdPatch::EndsWith")); - - // Contains - std::set test_set { 1, 2, 3, 4, 6, 7 }; - Assert(YYCC::StdPatch::Contains(test_set, static_cast(1)), YYCC_U8("YYCC::StdPatch::Contains")); - Assert(!YYCC::StdPatch::Contains(test_set, static_cast(5)), YYCC_U8("YYCC::StdPatch::Contains")); - std::map test_map { { 1, 1.0f }, { 4, 4.0f } }; - Assert(YYCC::StdPatch::Contains(test_map, static_cast(1)), YYCC_U8("YYCC::StdPatch::Contains")); - Assert(!YYCC::StdPatch::Contains(test_map, static_cast(5)), YYCC_U8("YYCC::StdPatch::Contains")); - - } - enum class TestEnum : int8_t { Test1, Test2, Test3 }; diff --git a/testbench/yycc/windows/dialog.cpp b/testbench/yycc/windows/dialog.cpp index 9641a7d..86bfe49 100644 --- a/testbench/yycc/windows/dialog.cpp +++ b/testbench/yycc/windows/dialog.cpp @@ -7,10 +7,49 @@ namespace yycctest::windows::dialog { #if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + TEST(WindowsDialog, Normal) { + // TODO: + // I temporaryly disable all dialog open functions in this function after testing them. + // Because they need human to operate them to finish the test. + // Once I find a better way to do automatic test (maybe send message to these dialogs to close them?) + // I will add them back. - // TEST(WindowsDialog, Normal) { - - // } + // Prepare parameters + DIALOG::FileDialog params; + auto& filters = params.configre_file_types(); + filters.add_filter(u8"Microsoft Word (*.docx; *.doc)", {u8"*.docx", u8"*.doc"}); + filters.add_filter(u8"Microsoft Excel (*.xlsx; *.xls)", {u8"*.xlsx", u8"*.xls"}); + filters.add_filter(u8"Microsoft PowerPoint (*.pptx; *.ppt)", {u8"*.pptx", u8"*.ppt"}); + filters.add_filter(u8"Text File (*.txt)", {u8"*.txt"}); + filters.add_filter(u8"All Files (*.*)", {u8"*.*"}); + params.set_default_file_type_index(1u); + + //// Open file + //{ + // auto rv = DIALOG::open_file(params); + // EXPECT_TRUE(rv.has_value()); + //} + //// Open files + //{ + // auto rv = DIALOG::open_files(params); + // EXPECT_TRUE(rv.has_value()); + //} + //// Save file + //{ + // auto rv = DIALOG::save_file(params); + // EXPECT_TRUE(rv.has_value()); + //} + + // Clear file filters for following operations + params.clear(); + params.set_default_file_type_index(0u); + + //// Open folder + //{ + // auto rv = DIALOG::open_folder(params); + // EXPECT_TRUE(rv.has_value()); + //} + } #endif }