diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7932d4f..34eb5c7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,9 @@ PRIVATE yycc/string/op.cpp yycc/patch/fopen.cpp yycc/rust/panic.cpp + yycc/windows/com.cpp + #yycc/windows/dialog.cpp + yycc/windows/winfct.cpp yycc/encoding/stl.cpp yycc/encoding/windows.cpp yycc/encoding/iconv.cpp @@ -52,6 +55,9 @@ FILES yycc/rust/result.hpp yycc/windows/import_guard_head.hpp yycc/windows/import_guard_tail.hpp + yycc/windows/com.hpp + #yycc/windows/dialog.hpp + yycc/windows/winfct.hpp yycc/constraint.hpp yycc/constraint/builder.hpp yycc/encoding/stl.hpp @@ -115,7 +121,8 @@ PUBLIC $<$:/utf-8> # Order preprocessor conformance mode (fix __VA_OPT__ error in MSVC) $<$:/Zc:preprocessor> - + # Resolve MSVC __cplusplus macro value error. + $<$:/Zc:__cplusplus> ) # Fix GCC std::stacktrace link error diff --git a/src/YYCCLegacy/COMHelper.cpp b/src/YYCCLegacy/COMHelper.cpp deleted file mode 100644 index 4e01629..0000000 --- a/src/YYCCLegacy/COMHelper.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "COMHelper.hpp" -#if defined(YYCC_OS_WINDOWS) - -namespace YYCC::COMHelper { - - /** - * @brief The guard for initialize COM environment. - * @details This class will try initializing COM environment by calling CoInitialize when constructing, - * and it also will try uninitializing COM environment when destructing. - * If initialization failed, uninitialization will not be executed. - */ - class ComGuard { - public: - ComGuard() : m_HasInit(false) { - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (SUCCEEDED(hr)) m_HasInit = true; - } - ~ComGuard() { - if (m_HasInit) { - CoUninitialize(); - } - } - - bool IsInitialized() const { - return m_HasInit; - } - - protected: - bool m_HasInit; - }; - - /** - * @brief The instance of COM environment guard. - * @details Dialog related function need COM environment, - * so we need initializing COM environment when loading this module, - * and uninitializing COM environment when we no longer use this module. - * So we use a static instance in here. - * And make it be const so no one can change it. - */ - static const ComGuard c_ComGuard {}; - - bool IsInitialized() { - return c_ComGuard.IsInitialized(); - } - -} - -#endif \ No newline at end of file diff --git a/src/YYCCLegacy/COMHelper.hpp b/src/YYCCLegacy/COMHelper.hpp deleted file mode 100644 index 8bb6b33..0000000 --- a/src/YYCCLegacy/COMHelper.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once -#include "YYCCInternal.hpp" -#if defined(YYCC_OS_WINDOWS) - -#include - -#include "WinImportPrefix.hpp" -#include -#include -#include "WinImportSuffix.hpp" - -/** - * @brief Windows COM related types and checker. - * @details - * This namespace is Windows specific. - * In other platforms, this whole namespace will be unavailable. - * - * See also \ref com_helper. -*/ -namespace YYCC::COMHelper { - - /// @brief C++ standard deleter for every COM interfaces inheriting IUnknown. - class ComPtrDeleter { - public: - ComPtrDeleter() {} - void operator() (IUnknown* com_ptr) { - if (com_ptr != nullptr) { - com_ptr->Release(); - } - } - }; - - /// @brief Smart unique pointer of \c IFileDialog - using SmartIFileDialog = std::unique_ptr; - /// @brief Smart unique pointer of \c IFileOpenDialog - using SmartIFileOpenDialog = std::unique_ptr; - /// @brief Smart unique pointer of \c IShellItem - using SmartIShellItem = std::unique_ptr; - /// @brief Smart unique pointer of \c IShellItemArray - using SmartIShellItemArray = std::unique_ptr; - /// @brief Smart unique pointer of \c IShellFolder - using SmartIShellFolder = std::unique_ptr; - - /// @brief C++ standard deleter for almost raw pointer used in COM which need to be free by CoTaskMemFree() - class CoTaskMemDeleter { - public: - CoTaskMemDeleter() {} - void operator() (void* com_ptr) { - if (com_ptr != nullptr) { - CoTaskMemFree(com_ptr); - } - } - }; - - /// @brief Smart unique pointer of COM created \c WCHAR sequence. - using SmartLPWSTR = std::unique_ptr, CoTaskMemDeleter>; - - /** - * @brief Check whether COM environment has been initialized. - * @return True if it is, otherwise false. - * @remarks - * This function will call corresponding function of COM Guard. - * Do not remove this function and you must preserve at least one reference to this function in final program. - * Some compiler will try to drop COM Guard in final program if no reference to it and it will cause the initialization of COM environment failed. - * This is the reason why I order you do the things said above. - */ - bool IsInitialized(); - -} - -#endif diff --git a/src/yycc/encoding/windows.cpp b/src/yycc/encoding/windows.cpp index 87aaec2..710b323 100644 --- a/src/yycc/encoding/windows.cpp +++ b/src/yycc/encoding/windows.cpp @@ -143,16 +143,17 @@ namespace yycc::encoding::windows { // Due to the shitty design of mbrtoc16, it forcely assume that passed string is null-terminated. // And the third argument should >= 1. // However, our given string is string view which do not have null-terminated guaranteen. - // + // // So we manually check whether we have reach the tail of string and simulate a fake null terminal. // If string is still processing, we pass given string. // If we have reach the tail of string, we pass our homemade NULL_TERMINAL to this function to make it works normally. - // + // // This is a stupid polyfill, however, it I do not do this, // there is a bug that the second part of surrogate pair will be dropped in final string, // if there is a Unicode character located at the tail of string which need surrogate pair to be presented. static const char NULL_TERMINAL = '\0'; - while (size_t rc = std::mbrtoc16(&c16, (ptr < end ? ptr : &NULL_TERMINAL), (ptr < end ? end - ptr : sizeof(NULL_TERMINAL)), &state)) { + while ( + size_t rc = std::mbrtoc16(&c16, (ptr < end ? ptr : &NULL_TERMINAL), (ptr < end ? end - ptr : sizeof(NULL_TERMINAL)), &state)) { if (rc == (size_t) -1) return std::unexpected(ConvError::EncodeUtf8); else if (rc == (size_t) -2) return std::unexpected(ConvError::IncompleteUtf8); else if (rc == (size_t) -3) dst.push_back(c16); // from earlier surrogate pair @@ -174,7 +175,7 @@ namespace yycc::encoding::windows { size_t rc = 1; // Assign it to ONE to avoid mismatching surrogate pair checker when string is empty. for (char16_t c : src) { rc = std::c16rtomb(mbout, c, &state); - + if (rc == (size_t) -1) return std::unexpected(ConvError::InvalidUtf16); else dst.append(reinterpret_cast(mbout), rc); } diff --git a/src/yycc/windows/com.cpp b/src/yycc/windows/com.cpp new file mode 100644 index 0000000..3347793 --- /dev/null +++ b/src/yycc/windows/com.cpp @@ -0,0 +1,46 @@ +#include "com.hpp" +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + +namespace yycc::windows::com { + + /** + * @brief The guard for initialize COM environment. + * @details This class will try initializing COM environment by calling CoInitialize when constructing, + * and it also will try uninitializing COM environment when destructing. + * If initialization failed, uninitialization will not be executed. + */ + class ComGuard { + public: + ComGuard() : m_HasInit(false) { + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (SUCCEEDED(hr)) m_HasInit = true; + } + ~ComGuard() { + if (m_HasInit) { + CoUninitialize(); + } + } + + bool IsInitialized() const { return m_HasInit; } + + protected: + bool m_HasInit; + }; + + /** + * @brief The instance of COM environment guard. + * @details Dialog related function need COM environment, + * so we need initializing COM environment when loading this module, + * and uninitializing COM environment when we no longer use this module. + * So we use a static instance in here. + * And make it be const so no one can change it. + */ + static const ComGuard COM_GUARD{}; + + bool is_initialized() { + return COM_GUARD.IsInitialized(); + } + +} // namespace yycc::windows::com + +#endif diff --git a/src/yycc/windows/com.hpp b/src/yycc/windows/com.hpp new file mode 100644 index 0000000..9805264 --- /dev/null +++ b/src/yycc/windows/com.hpp @@ -0,0 +1,70 @@ +#pragma once +#include "../macro/os_detector.hpp" +#include "../macro/stl_detector.hpp" +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + +#include + +#include "import_guard_head.hpp" +#include +#include +#include "import_guard_tail.hpp" + +/** + * @brief Windows COM related types and checker. + * @details + * This namespace is Windows specific. + * In other platforms, this whole namespace will be unavailable. +*/ +namespace yycc::windows::com { + + /// @brief C++ standard deleter for every COM interfaces inheriting IUnknown. + class ComPtrDeleter { + public: + ComPtrDeleter() {} + void operator()(IUnknown* com_ptr) { + if (com_ptr != nullptr) { + com_ptr->Release(); + } + } + }; + + /// @brief Smart unique pointer of \c IFileDialog + using SmartIFileDialog = std::unique_ptr; + /// @brief Smart unique pointer of \c IFileOpenDialog + using SmartIFileOpenDialog = std::unique_ptr; + /// @brief Smart unique pointer of \c IShellItem + using SmartIShellItem = std::unique_ptr; + /// @brief Smart unique pointer of \c IShellItemArray + using SmartIShellItemArray = std::unique_ptr; + /// @brief Smart unique pointer of \c IShellFolder + using SmartIShellFolder = std::unique_ptr; + + /// @brief C++ standard deleter for almost raw pointer used in COM which need to be free by CoTaskMemFree() + class CoTaskMemDeleter { + public: + CoTaskMemDeleter() {} + void operator()(void* com_ptr) { + if (com_ptr != nullptr) { + CoTaskMemFree(com_ptr); + } + } + }; + + /// @brief Smart unique pointer of COM created \c WCHAR sequence. + using SmartLPWSTR = std::unique_ptr, CoTaskMemDeleter>; + + /** + * @brief Check whether COM environment has been initialized. + * @return True if it is, otherwise false. + * @remarks + * This function will call corresponding function of COM Guard. + * Do not remove this function and you must preserve at least one reference to this function in final program. + * Some compiler will try to drop COM Guard in final program if no reference to it and it will cause the initialization of COM environment failed. + * This is the reason why I order you do the things said above. + */ + bool is_initialized(); + +} // namespace yycc::windows::com + +#endif diff --git a/src/yycc/windows/dialog.cpp b/src/yycc/windows/dialog.cpp new file mode 100644 index 0000000..ea10029 --- /dev/null +++ b/src/yycc/windows/dialog.cpp @@ -0,0 +1,348 @@ +#include "dialog.hpp" +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + +namespace yycc::windows::dialog { + +#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/yycc/windows/dialog.hpp b/src/yycc/windows/dialog.hpp new file mode 100644 index 0000000..e9a597c --- /dev/null +++ b/src/yycc/windows/dialog.hpp @@ -0,0 +1,304 @@ +#pragma once +#include "../macro/os_detector.hpp" +#include "../macro/stl_detector.hpp" +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + +#include "../macro/class_copy_move.hpp" +#include "com.hpp" +#include +#include +#include + +#include "import_guard_head.hpp" +#include +#include +#include "import_guard_tail.hpp" + +/** + * @brief The namespace providing Windows universal dialog features. + * @details + * This namespace only available on Windows platform. + * See also \ref dialog_helper. +*/ +namespace yycc::windows::dialog { + + /** + * @private + * @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_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; + + /// @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_DELETE_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 = std::u8string; + using FilterPair = std::pair; + + std::vector m_Filters; + }; + + /** + * @private + * @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_DELETE_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; + 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(); + } + }; + + /** + * @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_DELETE_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; + std::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, std::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, std::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, std::u8string& ret); + +} + +#endif diff --git a/src/yycc/windows/winfct.cpp b/src/yycc/windows/winfct.cpp new file mode 100644 index 0000000..3235d0e --- /dev/null +++ b/src/yycc/windows/winfct.cpp @@ -0,0 +1,158 @@ +#include "winfct.hpp" +#if defined(YYCC_OS_WINDOWS) + +#include "../encoding/windows.hpp" +#include + +#if defined(YYCC_STL_MSSTL) +#include "com.hpp" +#endif + +#define ENC ::yycc::encoding::windows +#define COM ::yycc::windows::com + +namespace yycc::windows::winfct { + + WinFctResult get_current_module() { + // Reference: https://stackoverflow.com/questions/557081/how-do-i-get-the-hmodule-for-the-currently-executing-code + HMODULE hModule = NULL; + BOOL rv = ::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, // get address but do not inc ref counter. + (LPCWSTR) get_current_module, + &hModule); + if (rv) return hModule; + else return std::unexpected(WinFctError::Backend); + } + + WinFctResult get_temp_directory() { + // create wchar buffer for receiving the temp path. + std::wstring wpath(MAX_PATH + 1u, L'\0'); + DWORD expected_size; + + // fetch temp folder + while (true) { + if ((expected_size = ::GetTempPathW(static_cast(wpath.size()), wpath.data())) == 0) { + // failed, return + return std::unexpected(WinFctError::Backend); + } + + if (expected_size > static_cast(wpath.size())) { + // buffer is too short, need enlarge and do fetching again + wpath.resize(expected_size); + } else { + // ok. shrink to real length. break while + wpath.resize(expected_size); + break; + } + } + + // convert to utf8 and return + return ENC::to_utf8(wpath).transform_error([](auto err) { return WinFctError::Encoding; }); + } + + WinFctResult get_module_file_name(HINSTANCE hModule) { + // create wchar buffer for receiving the temp path. + std::wstring wpath(MAX_PATH + 1u, L'\0'); + DWORD copied_size; + + while (true) { + if ((copied_size = ::GetModuleFileNameW(hModule, wpath.data(), static_cast(wpath.size()))) == 0) { + // failed, return + return std::unexpected(WinFctError::Backend); + } + + // check insufficient buffer + if (::GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + // buffer is not enough, enlarge it and try again. + wpath.resize(wpath.size() + MAX_PATH); + } else { + // ok. shrink to real length. break while + wpath.resize(copied_size); + break; + } + } + + // convert to utf8 and return + return ENC::to_utf8(wpath).transform_error([](auto err) { return WinFctError::Encoding; }); + } + + bool is_valid_code_page(UINT code_page) { + CPINFOEXW cpinfo; + return ::GetCPInfoExW(code_page, 0, &cpinfo); + } + + WinFctResult copy_file(const std::u8string_view& lpExistingFileName, const std::u8string_view& lpNewFileName, BOOL bFailIfExists) { + auto wExistingFileName = ENC::to_wchar(lpExistingFileName); + auto wNewFileName = ENC::to_wchar(lpNewFileName); + if (!(wExistingFileName.has_value() && wNewFileName.has_value())) { + return std::unexpected(WinFctError::Encoding); + } + + if (!::CopyFileW(wExistingFileName.value().c_str(), wNewFileName.value().c_str(), bFailIfExists)) { + return std::unexpected(WinFctError::Backend); + } + + return {}; + } + + WinFctResult move_file(const std::u8string_view& lpExistingFileName, const std::u8string_view& lpNewFileName) { + auto wExistingFileName = ENC::to_wchar(lpExistingFileName); + auto wNewFileName = ENC::to_wchar(lpNewFileName); + if (!(wExistingFileName.has_value() && wNewFileName.has_value())) { + return std::unexpected(WinFctError::Encoding); + } + + if (!::MoveFileW(wExistingFileName.value().c_str(), wNewFileName.value().c_str())) { + return std::unexpected(WinFctError::Backend); + } + + return {}; + } + + WinFctResult delete_file(const std::u8string_view& lpFileName) { + auto wFileName = ENC::to_wchar(lpFileName); + if (!wFileName.has_value()) { + return std::unexpected(WinFctError::Encoding); + } + + if (!::DeleteFileW(wFileName.value().c_str())) { + return std::unexpected(WinFctError::Backend); + } + + return {}; + } + +#if defined(YYCC_STL_MSSTL) + + WinFctResult get_known_path(KnownDirectory path_type) { + // check whether com initialized + if (!COM::is_initialized()) return std::unexpected(WinFctError::NoCom); + + // get folder id according to type + const KNOWNFOLDERID* pId; + switch (path_type) { + case KnownDirectory::LocalAppData: + pId = &FOLDERID_LocalAppData; + break; + case KnownDirectory::AppData: + pId = &FOLDERID_RoamingAppData; + break; + default: + throw std::logic_error("unknow known directory type"); + } + + // fetch path + LPWSTR raw_known_path; + HRESULT hr = SHGetKnownFolderPath(*pId, KF_FLAG_CREATE, NULL, &raw_known_path); + if (FAILED(hr)) return std::unexpected(WinFctError::Backend); + COM::SmartLPWSTR known_path(raw_known_path); + + // convert to utf8 and return + return ENC::to_utf8(known_path.get()).transform_error([](auto err) { return WinFctError::Encoding; }); + } + +#endif + +} // namespace yycc::windows::winfct + +#endif diff --git a/src/yycc/windows/winfct.hpp b/src/yycc/windows/winfct.hpp new file mode 100644 index 0000000..06415da --- /dev/null +++ b/src/yycc/windows/winfct.hpp @@ -0,0 +1,116 @@ +#pragma once +#include "../macro/os_detector.hpp" +#include "../macro/stl_detector.hpp" +#if defined(YYCC_OS_WINDOWS) + +#include +#include +#include + +#include "import_guard_head.hpp" +#include +#include "import_guard_tail.hpp" + +namespace yycc::windows::winfct { + + /// @brief All errors occur in this module. + enum class WinFctError { + Backend, ///< Error occurs when calling Win32 functions. + Encoding, ///< Can not perform encoding convertion. + NoCom, ///< No COM environment. + }; + + /// @brief The result type used in this module. + template + using WinFctResult = std::expected; + + /** + * @brief Get Windows used HANDLE for current module. + * @details + * If your target is EXE, the current module simply is your program self. + * However, if your target is DLL, the current module is your DLL, not the EXE loading your DLL. + * + * This function is frequently used by DLL. + * Because some design need the HANDLE of current module, not the host EXE loading your DLL. + * For example, you may want to get the path of your built DLL, or fetch resources from your DLL at runtime, + * then you should pass current module HANDLE, not NULL or the HANDLE of EXE. + * @return A Windows HANDLE pointing to current module, or error occurs. + */ + WinFctResult get_current_module(); + + /** + * @brief Get path to Windows temporary directory. + * @details Windows temporary directory usually is the target of \%TEMP\%. + * @return Fetched UTF8 encoded path to Windows temporary directory, or error occurs. + */ + WinFctResult get_temp_directory(); + + /** + * @brief Get the file name of given module HANDLE + * @param[in] hModule + * The HANDLE to the module where you want to get file name. + * It is same as the HANDLE parameter of Win32 \c GetModuleFileName. + * @param[out] ret The variable receiving UTF8 encoded file name of given module. + * @return Fetched UTF8 encoded file name of given module, or error occurs. + */ + WinFctResult get_module_file_name(HINSTANCE hModule); + + /** + * @brief Check whether given code page number is a valid one. + * @param[in] code_page The code page number. + * @return True if it is valid, otherwise false. + */ + bool is_valid_code_page(UINT code_page); + + /** + * @brief Copies an existing file to a new file. + * @param[in] lpExistingFileName The name of an existing file. + * @param[in] lpNewFileName The name of the new file. + * @param[in] bFailIfExists + * If this parameter is TRUE and the new file specified by \c lpNewFileName already exists, the function fails. + * If this parameter is FALSE and the new file already exists, the function overwrites the existing file and succeeds. + * @return Nothing or error occurs. If function failed with backend error, caller can call \c GetLastError for more details. + * @remarks Same as Windows \c CopyFile: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-copyfilew + */ + WinFctResult copy_file(const std::u8string_view& lpExistingFileName, const std::u8string_view& lpNewFileName, BOOL bFailIfExists); + + /** + * @brief Moves an existing file or a directory, including its children. + * @param[in] lpExistingFileName The current name of the file or directory on the local computer. + * @param[in] lpNewFileName + * The new name for the file or directory. The new name must not already exist. + * A new file may be on a different file system or drive. A new directory must be on the same drive. + * @return Nothing or error occurs. If function failed with backend error, caller can call \c GetLastError for more details. + * @remarks Same as Windows \c MoveFile: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefilew + */ + WinFctResult move_file(const std::u8string_view& lpExistingFileName, const std::u8string_view& lpNewFileName); + + /** + * @brief Deletes an existing file. + * @param[in] lpFileName The name of the file to be deleted. + * @return Nothing or error occurs. If function failed with backend error, caller can call \c GetLastError for more details. + * @remarks Same as Windows \c DeleteFile: https://learn.microsoft.com/e-us/windows/win32/api/winbase/nf-winbase-deletefile + */ + WinFctResult delete_file(const std::u8string_view& lpFileName); + +#if defined(YYCC_STL_MSSTL) + + /// @brief The known directory type in Windows. + enum class KnownDirectory { + LocalAppData, ///< The path \%LOCALAPPDATA\% pointed. + AppData, ///< The path \%APPDATA\% pointed. + }; + + /** + * @brief Get the path to \%LOCALAPPDATA\%. + * @details \%LOCALAPPDATA\% usually was used as putting local app data files + * @param[out] ret The variable receiving UTF8 encoded path to LOCALAPPDATA. + * @return True if success, otherwise false. + */ + WinFctResult get_known_path(KnownDirectory path_type); + +#endif + +} + +#endif diff --git a/testbench/CMakeLists.txt b/testbench/CMakeLists.txt index 4734267..376bd89 100644 --- a/testbench/CMakeLists.txt +++ b/testbench/CMakeLists.txt @@ -22,6 +22,9 @@ PRIVATE yycc/encoding/stl.cpp yycc/encoding/windows.cpp yycc/encoding/iconv.cpp + yycc/windows/com.cpp + #yycc/windows/dialog.cpp + yycc/windows/winfct.cpp yycc/carton/pycodec.cpp ) diff --git a/testbench/yycc/windows/com.cpp b/testbench/yycc/windows/com.cpp new file mode 100644 index 0000000..db8b20f --- /dev/null +++ b/testbench/yycc/windows/com.cpp @@ -0,0 +1,16 @@ +#include +#include +#include + +#define COM ::yycc::windows::com + +namespace yycctest::windows::com { +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + + TEST(WindowsCom, IsInitialized) { + // COM environment should always be ready. + EXPECT_TRUE(COM::is_initialized()); + } + +#endif +} diff --git a/testbench/yycc/windows/dialog.cpp b/testbench/yycc/windows/dialog.cpp new file mode 100644 index 0000000..9641a7d --- /dev/null +++ b/testbench/yycc/windows/dialog.cpp @@ -0,0 +1,16 @@ +#include +#include +#include + +#define DIALOG ::yycc::windows::dialog + +namespace yycctest::windows::dialog { +#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL) + + + // TEST(WindowsDialog, Normal) { + + // } + +#endif +} diff --git a/testbench/yycc/windows/winfct.cpp b/testbench/yycc/windows/winfct.cpp new file mode 100644 index 0000000..35e7289 --- /dev/null +++ b/testbench/yycc/windows/winfct.cpp @@ -0,0 +1,48 @@ +#include +#include +#include + +#define WINFCT ::yycc::windows::winfct + +namespace yycctest::windows::winfct { +#if defined(YYCC_OS_WINDOWS) + + TEST(WindowsWinFct, GetCurrentModule) { + auto rv = WINFCT::get_current_module(); + EXPECT_TRUE(rv.has_value()); + } + + TEST(WindowsWinFct, GetTempDirectory) { + auto rv = WINFCT::get_temp_directory(); + EXPECT_TRUE(rv.has_value()); + } + + TEST(WindowsWinFct, GetModuleFileName) { + auto handle = WINFCT::get_current_module(); + ASSERT_TRUE(handle.has_value()); + + auto rv = WINFCT::get_module_file_name(handle.value()); + EXPECT_TRUE(rv.has_value()); + } + + TEST(WindowsWinFct, IsValidCodePage) { + EXPECT_TRUE(WINFCT::is_valid_code_page(437)); + EXPECT_TRUE(WINFCT::is_valid_code_page(65001)); + + EXPECT_FALSE(WINFCT::is_valid_code_page(6161)); + } + +#if defined(YYCC_STL_MSSTL) + + TEST(WindowsWinFct, GetKnownPath) { + auto rv = WINFCT::get_known_path(WINFCT::KnownDirectory::LocalAppData); + EXPECT_TRUE(rv.has_value()); + } + +#endif + + // YYC MARK: + // I can't test CopyFile, MoveFile and DeleteFile. + +#endif +} // namespace yycctest::windows::winfct