8 Commits

Author SHA1 Message Date
ccd0219ead refactor: migrate console helper.
- migrate csharp style console helper.
- i just do a simple migration and mark it as deprecated. it should works like 1.x version.
2025-08-22 23:28:17 +08:00
4bfba6f243 feat: add windows-spec console patch 2025-08-22 21:51:32 +08:00
9e994dd4f0 refactor: bring utf8 patch for std::ostream back. 2025-08-22 21:28:29 +08:00
d6034f8cb0 fix: fix minor issue of testbench
- fix testbench minor issue.
- delete migrated source code files.
2025-08-22 21:09:57 +08:00
0694d923f3 test: finish testbench for tabulate 2025-08-21 14:32:51 +08:00
580b096cb3 fix: fix bug for windows dialog 2025-08-21 11:00:33 +08:00
f9365481b9 fix: fix all build issue of dialog namespace but not test. 2025-08-21 10:26:28 +08:00
15aade052f refactor: expand ugly statement from while statement header to body. 2025-08-20 19:32:44 +08:00
26 changed files with 1358 additions and 1912 deletions

View File

@ -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

View File

@ -13,12 +13,14 @@ PRIVATE
# Sources
yycc/string/reinterpret.cpp
yycc/string/op.cpp
yycc/string/stream.cpp
yycc/patch/fopen.cpp
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/windows/console.cpp
yycc/encoding/stl.cpp
yycc/encoding/windows.cpp
yycc/encoding/iconv.cpp
@ -27,6 +29,7 @@ PRIVATE
yycc/carton/termcolor.cpp
yycc/carton/wcwidth.cpp
yycc/carton/tabulate.cpp
yycc/carton/csconsole.cpp
)
target_sources(YYCCommonplace
PUBLIC
@ -45,6 +48,7 @@ FILES
yycc/flag_enum.hpp
yycc/string/reinterpret.hpp
yycc/string/op.hpp
yycc/string/stream.hpp
yycc/patch/ptr_pad.hpp
yycc/patch/fopen.hpp
yycc/num/parse.hpp
@ -61,8 +65,9 @@ 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/windows/console.hpp
yycc/constraint.hpp
yycc/constraint/builder.hpp
yycc/encoding/stl.hpp
@ -73,6 +78,7 @@ FILES
yycc/carton/termcolor.hpp
yycc/carton/wcwidth.hpp
yycc/carton/tabulate.hpp
yycc/carton/csconsole.hpp
)
# Setup header infomations
target_include_directories(YYCCommonplace

View File

@ -1,282 +0,0 @@
#include "ConsoleHelper.hpp"
#include "EncodingHelper.hpp"
#include "StringHelper.hpp"
#include <iostream>
// Include Windows used headers in Windows.
#if defined(YYCC_OS_WINDOWS)
#include "WinImportPrefix.hpp"
#include <Windows.h>
#include <io.h>
#include <fcntl.h>
#include "WinImportSuffix.hpp"
#endif
namespace YYCC::ConsoleHelper {
#pragma region Windows Specific Functions
#if defined(YYCC_OS_WINDOWS)
static bool RawEnableColorfulConsole(FILE* fs) {
if (!_isatty(_fileno(fs))) return false;
HANDLE h_output;
DWORD dw_mode;
h_output = (HANDLE)_get_osfhandle(_fileno(fs));
if (!GetConsoleMode(h_output, &dw_mode)) return false;
if (!SetConsoleMode(h_output, dw_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) return false;
return true;
}
/*
Reference:
* https://stackoverflow.com/questions/45575863/how-to-print-utf-8-strings-to-stdcout-on-windows
* https://stackoverflow.com/questions/69830460/reading-utf-8-input
There is 3 way to make Windows console enable UTF8 mode.
First one is calling SetConsoleCP and SetConsoleOutputCP.
The side effect of this is std::cin and std::cout is broken,
however there is a patch for this issue.
Second one is calling _set_mode with _O_U8TEXT or _O_U16TEXT to enable Unicode mode for Windows console.
This also have side effect which is stronger than first one.
All puts family functions (ASCII-based output functions) will throw assertion exception.
You only can use putws family functions (wide-char-based output functions).
However these functions can not be used without calling _set_mode in Windows design.
There still is another method, using WriteConsoleW directly visiting console.
This function family can output correct string without calling any extra functions!
This method is what we adopted.
*/
template<bool _bIsConsole>
static yycc_u8string WinConsoleRead(HANDLE hStdIn) {
using _TChar = std::conditional_t<_bIsConsole, wchar_t, char>;
// Prepare an internal buffer because the read data may not be fully used.
// For example, we may read x\ny in a single calling but after processing \n, this function will return
// so y will temporarily stored in this internal buffer for next using.
// Thus this function is not thread safe.
static std::basic_string<_TChar> internal_buffer;
// create return value buffer
std::basic_string<_TChar> return_buffer;
// Prepare some variables
DWORD dwReadNumberOfChars;
_TChar szReadChars[64];
size_t eol_pos;
// try fetching EOL
while (true) {
// if internal buffer is empty,
// try fetching it.
if (internal_buffer.empty()) {
// console and non-console use different method to read.
if constexpr (_bIsConsole) {
// console handle, use ReadConsoleW.
// read from console, the read data is wchar based
if (!ReadConsoleW(hStdIn, szReadChars, sizeof(szReadChars) / sizeof(_TChar), &dwReadNumberOfChars, NULL))
break;
} else {
// anything else, use ReadFile instead.
// the read data is utf8 based
if (!ReadFile(hStdIn, szReadChars, sizeof(szReadChars), &dwReadNumberOfChars, NULL))
break;
}
// send to internal buffer
if (dwReadNumberOfChars == 0) break;
internal_buffer.append(szReadChars, dwReadNumberOfChars);
}
// try finding EOL in internal buffer
if constexpr (std::is_same_v<_TChar, char>) eol_pos = internal_buffer.find_first_of('\n');
else eol_pos = internal_buffer.find_first_of(L'\n');
// check finding result
if (eol_pos == std::wstring::npos) {
// the whole string do not include EOL, fully appended to return value
return_buffer += internal_buffer;
internal_buffer.clear();
// need more data, continue while
} else {
// split result
// push into result and remain some in internal buffer.
return_buffer.append(internal_buffer, 0u, eol_pos);
internal_buffer.erase(0u, eol_pos + 1u); // +1 because EOL take one place.
// break while mean success finding
break;
}
}
// post-process for return value
yycc_u8string real_return_buffer;
if constexpr (_bIsConsole) {
// console mode need convert wchar to utf8
YYCC::EncodingHelper::WcharToUTF8(return_buffer, real_return_buffer);
} else {
// non-console just copt the result
real_return_buffer = EncodingHelper::ToUTF8(return_buffer);
}
// every mode need delete \r words
YYCC::StringHelper::Replace(real_return_buffer, YYCC_U8("\r"), YYCC_U8(""));
// return value
return real_return_buffer;
}
static void WinConsoleWrite(const yycc_u8string& strl, bool to_stderr) {
// Prepare some Win32 variables
// fetch stdout handle first
HANDLE hStdOut = GetStdHandle(to_stderr ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
DWORD dwConsoleMode;
DWORD dwWrittenNumberOfChars;
// if stdout was redirected, this handle may point to a file handle or anything else,
// WriteConsoleW can not write data into such scenario, so we need check whether this handle is console handle
if (GetConsoleMode(hStdOut, &dwConsoleMode)) {
// console handle, use WriteConsoleW.
// convert utf8 string to wide char first
std::wstring wstrl(YYCC::EncodingHelper::UTF8ToWchar(strl));
size_t wstrl_size = wstrl.size();
// write string with size check
if (wstrl_size <= std::numeric_limits<DWORD>::max()) {
WriteConsoleW(hStdOut, wstrl.c_str(), static_cast<DWORD>(wstrl_size), &dwWrittenNumberOfChars, NULL);
}
} else {
// anything else, use WriteFile instead.
// WriteFile do not need extra convertion, because it is direct writing.
// check whether string length is overflow
size_t strl_size = strl.size() * sizeof(yycc_u8string::value_type);
// write string with size check
if (strl_size <= std::numeric_limits<DWORD>::max()) {
WriteFile(hStdOut, strl.c_str(), static_cast<DWORD>(strl_size), &dwWrittenNumberOfChars, NULL);
}
}
}
#endif
#pragma endregion
bool EnableColorfulConsole() {
#if defined(YYCC_OS_WINDOWS)
bool ret = true;
ret &= RawEnableColorfulConsole(stdout);
ret &= RawEnableColorfulConsole(stderr);
return ret;
#else
// just return true and do nothing
return true;
#endif
}
yycc_u8string ReadLine() {
#if defined(YYCC_OS_WINDOWS)
// get stdin mode
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
// use different method to get according to whether stdin is redirected
DWORD dwConsoleMode;
if (GetConsoleMode(hStdIn, &dwConsoleMode)) {
return WinConsoleRead<true>(hStdIn);
} else {
return WinConsoleRead<false>(hStdIn);
}
#else
// in linux, directly use C++ function to fetch.
std::string cmd;
if (std::getline(std::cin, cmd).fail()) cmd.clear();
return EncodingHelper::ToUTF8(cmd);
#endif
}
template<bool bNeedFmt, bool bIsErr, bool bHasEOL>
static void RawWrite(const yycc_char8_t* u8_fmt, va_list argptr) {
// Buiild string need to be written first
// If no format string or plain string for writing, return.
if (u8_fmt == nullptr) return;
// Build or simply copy string
yycc_u8string strl;
if constexpr (bNeedFmt) {
// treat as format string
va_list argcpy;
va_copy(argcpy, argptr);
strl = YYCC::StringHelper::VPrintf(u8_fmt, argcpy);
va_end(argcpy);
} else {
// treat as plain string
strl = u8_fmt;
}
// Checkout whether add EOL
if constexpr (bHasEOL) {
strl += YYCC_U8("\n");
}
#if defined(YYCC_OS_WINDOWS)
// call Windows specific writer
WinConsoleWrite(strl, bIsErr);
#else
// in linux, directly use C function to write.
std::fputs(EncodingHelper::ToOrdinary(strl.c_str()), bIsErr ? stderr : stdout);
#endif
}
void Format(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, false, false>(u8_fmt, argptr);
va_end(argptr);
}
void FormatLine(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, false, true>(u8_fmt, argptr);
va_end(argptr);
}
void Write(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, false, false>(u8_strl, empty);
}
void WriteLine(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, false, true>(u8_strl, empty);
}
void ErrFormat(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, true, false>(u8_fmt, argptr);
va_end(argptr);
}
void ErrFormatLine(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, true, true>(u8_fmt, argptr);
va_end(argptr);
}
void ErrWrite(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, true, false>(u8_strl, empty);
}
void ErrWriteLine(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, true, true>(u8_strl, empty);
}
}

View File

@ -1,92 +0,0 @@
#pragma once
#include "YYCCInternal.hpp"
#include <cstdio>
#include <string>
/**
* @brief The helper providing universal C\# style console function and other console related stuff
* @details
* For how to utilize this functions provided by this namespace, please view \ref console_helper.
*/
namespace YYCC::ConsoleHelper {
/**
* @brief Enable console color support for Windows.
* @details This actually is enable virtual console feature for \c stdout and \c stderr.
* @return True if success, otherwise false.
* @remarks
* This function only works on Windows and do nothing on other platforms such as Linux,
* because we assume all terminals existing on other platform support color feature as default.
*/
bool EnableColorfulConsole();
/**
* @brief Reads the next line of UTF8 characters from the standard input stream.
* @return
* The next line of UTF8 characters from the input stream.
* Empty string if user just press Enter key or function failed.
*/
yycc_u8string ReadLine();
/**
* @brief
* Writes the text representation of the specified object
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void Format(const yycc_char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void FormatLine(const yycc_char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void Write(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void WriteLine(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the text representation of the specified object
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void ErrFormat(const yycc_char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void ErrFormatLine(const yycc_char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ErrWrite(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ErrWriteLine(const yycc_char8_t* u8_strl);
}

View File

@ -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<const yycc_char8_t*> 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<UINT>::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<UINT>::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<UINT>(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<CommonFileDialogType EDialogType>
static bool CommonFileDialog(const FileDialog& params, std::vector<yycc_u8string>& 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<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::OpenFile>(params, cache);
if (isok) ret = cache.front();
return isok;
}
bool OpenMultipleFileDialog(const FileDialog& params, std::vector<yycc_u8string>& ret) {
return CommonFileDialog<CommonFileDialogType::OpenMultipleFiles>(params, ret);
}
bool SaveFileDialog(const FileDialog& params, yycc_u8string& ret) {
std::vector<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::SaveFile>(params, cache);
if (isok) ret = cache.front();
return isok;
}
bool OpenFolderDialog(const FileDialog& params, yycc_u8string& ret) {
std::vector<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::OpenFolder>(params, cache);
if (isok) ret = cache.front();
return isok;
}
#pragma endregion
}
#endif

View File

@ -1,312 +0,0 @@
#pragma once
#include "YYCCInternal.hpp"
#if defined(YYCC_OS_WINDOWS)
#include "COMHelper.hpp"
#include <string>
#include <vector>
#include <initializer_list>
#include "WinImportPrefix.hpp"
#include <Windows.h>
#include <shlobj_core.h>
#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<UINT>(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<WinFilterName, WinFilterModes>;
std::vector<WinFilterPair> m_WinFilters;
std::unique_ptr<COMDLG_FILTERSPEC[]> 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: <TT>Add(u8"Microsoft Word (*.doc; *.docx)", {u8"*.doc", u8"*.docx"})</TT>
*/
bool Add(const yycc_char8_t* filter_name, std::initializer_list<const yycc_char8_t*> 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<yycc_u8string>;
using FilterName = yycc_u8string;
using FilterPair = std::pair<FilterName, FilterModes>;
std::vector<FilterPair> 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<yycc_u8string>& 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

View File

@ -1,115 +0,0 @@
#include "WinFctHelper.hpp"
#if defined(YYCC_OS_WINDOWS)
#include "EncodingHelper.hpp"
#include "COMHelper.hpp"
namespace YYCC::WinFctHelper {
HMODULE GetCurrentModule() {
// Reference: https://stackoverflow.com/questions/557081/how-do-i-get-the-hmodule-for-the-currently-executing-code
HMODULE hModule = NULL;
::GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, // get address and do not inc ref counter.
(LPCWSTR)GetCurrentModule,
&hModule);
return hModule;
}
bool GetTempDirectory(yycc_u8string& ret) {
// 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<DWORD>(wpath.size()), wpath.data())) == 0) {
// failed, set to empty
return false;
}
if (expected_size > static_cast<DWORD>(wpath.size())) {
// buffer is too short, need enlarge and do fetching again
wpath.resize(expected_size);
} else {
// ok. shrink to real length, break while
break;
}
}
// resize result
wpath.resize(expected_size);
// convert to utf8 and return
return YYCC::EncodingHelper::WcharToUTF8(wpath, ret);
}
bool GetModuleFileName(HINSTANCE hModule, yycc_u8string& ret) {
// 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<DWORD>(wpath.size()))) == 0) {
// failed, return
return false;
}
// 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, break while
break;
}
}
// resize result
wpath.resize(copied_size);
// convert to utf8 and return
return YYCC::EncodingHelper::WcharToUTF8(wpath, ret);
}
bool GetLocalAppData(yycc_u8string& ret) {
// check whether com initialized
if (!COMHelper::IsInitialized()) return false;
// fetch path
LPWSTR _known_path;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, NULL, &_known_path);
if (FAILED(hr)) return false;
COMHelper::SmartLPWSTR known_path(_known_path);
// convert to utf8
return YYCC::EncodingHelper::WcharToUTF8(known_path.get(), ret);
}
bool IsValidCodePage(UINT code_page) {
CPINFOEXW cpinfo;
return ::GetCPInfoExW(code_page, 0, &cpinfo);
}
BOOL CopyFile(const yycc_u8string_view& lpExistingFileName, const yycc_u8string_view& lpNewFileName, BOOL bFailIfExists) {
std::wstring wExistingFileName, wNewFileName;
if (!YYCC::EncodingHelper::UTF8ToWchar(lpExistingFileName, wExistingFileName)) return FALSE;
if (!YYCC::EncodingHelper::UTF8ToWchar(lpNewFileName, wNewFileName)) return FALSE;
return ::CopyFileW(wExistingFileName.c_str(), wNewFileName.c_str(), bFailIfExists);
}
BOOL MoveFile(const yycc_u8string_view& lpExistingFileName, const yycc_u8string_view& lpNewFileName) {
std::wstring wExistingFileName, wNewFileName;
if (!YYCC::EncodingHelper::UTF8ToWchar(lpExistingFileName, wExistingFileName)) return FALSE;
if (!YYCC::EncodingHelper::UTF8ToWchar(lpNewFileName, wNewFileName)) return FALSE;
return ::MoveFileW(wExistingFileName.c_str(), wNewFileName.c_str());
}
BOOL DeleteFile(const yycc_u8string_view& lpFileName) {
std::wstring wFileName;
if (!YYCC::EncodingHelper::UTF8ToWchar(lpFileName, wFileName)) return FALSE;
return ::DeleteFileW(wFileName.c_str());
}
}
#endif

View File

@ -1,106 +0,0 @@
#pragma once
#include "YYCCInternal.hpp"
#if defined(YYCC_OS_WINDOWS)
#include <string>
#include "WinImportPrefix.hpp"
#include <Windows.h>
#include "WinImportSuffix.hpp"
/**
* @brief The helper providing assistance of Win32 functions.
* @details
* This helper is Windows specific.
* If current environment is not Windows, the whole namespace will be unavailable.
* See also \ref win_fct_helper
*/
namespace YYCC::WinFctHelper {
/**
* @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, NULL if failed.
*/
HMODULE GetCurrentModule();
/**
* @brief Get path to Windows temporary folder.
* @details Windows temporary folder usually is the target of \%TEMP\%.
* @param[out] ret The variable receiving UTF8 encoded path to Windows temp folder.
* @return True if success, otherwise false.
*/
bool GetTempDirectory(yycc_u8string& ret);
/**
* @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 True if success, otherwise false.
*/
bool GetModuleFileName(HINSTANCE hModule, yycc_u8string& ret);
/**
* @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.
*/
bool GetLocalAppData(yycc_u8string& ret);
/**
* @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 IsValidCodePage(UINT code_page);
/**
* @brief Copies an existing file to a new file.
* @param lpExistingFileName The name of an existing file.
* @param lpNewFileName The name of the new file.
* @param 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
* If the function succeeds, the return value is nonzero.
* If the function fails, the return value is zero. To get extended error information, call \c GetLastError.
* @remarks Same as Windows \c CopyFile: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-copyfilew
*/
BOOL CopyFile(const yycc_u8string_view& lpExistingFileName, const yycc_u8string_view& lpNewFileName, BOOL bFailIfExists);
/**
* @brief Moves an existing file or a directory, including its children.
* @param lpExistingFileName The current name of the file or directory on the local computer.
* @param 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
* If the function succeeds, the return value is nonzero.
* If the function fails, the return value is zero. To get extended error information, call \c GetLastError.
* @remarks Same as Windows \c MoveFile: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefilew
*/
BOOL MoveFile(const yycc_u8string_view& lpExistingFileName, const yycc_u8string_view& lpNewFileName);
/**
* @brief Deletes an existing file.
* @param lpFileName The name of the file to be deleted.
* @return
* If the function succeeds, the return value is nonzero.
* If the function fails, the return value is zero. To get extended error information, call \c GetLastError.
* @remarks Same as Windows \c DeleteFile: https://learn.microsoft.com/e-us/windows/win32/api/winbase/nf-winbase-deletefile
*/
BOOL DeleteFile(const yycc_u8string_view& lpFileName);
}
#endif

View File

@ -0,0 +1,247 @@
#include "csconsole.hpp"
#include "../macro/os_detector.hpp"
#include "../string/op.hpp"
#include "../string/reinterpret.hpp"
#include "../encoding/windows.hpp"
#include <iostream>
#include <cstdarg>
#include <cstdio>
#if defined(YYCC_OS_WINDOWS)
#include "../windows/import_guard_head.hpp"
#include <Windows.h>
#include "../windows/import_guard_tail.hpp"
#endif
#define OP ::yycc::string::op
#define REINTERPRET ::yycc::string::reinterpret
#define ENC ::yycc::encoding::windows
namespace yycc::carton::csconsole {
#pragma region Windows Console Specific Functions
#if defined(YYCC_OS_WINDOWS)
template<bool BIsConsole>
static std::u8string win_console_read(HANDLE hStdIn) {
using TChar = std::conditional_t<BIsConsole, wchar_t, char>;
// Prepare an internal buffer because the read data may not be fully used.
// For example, we may read x\ny in a single calling but after processing \n, this function will return
// so y will temporarily stored in this internal buffer for next using.
// Thus this function is not thread safe.
static std::basic_string<TChar> internal_buffer;
// create return value buffer
std::basic_string<TChar> return_buffer;
// Prepare some variables
DWORD dwReadNumberOfChars;
TChar szReadChars[64];
size_t eol_pos;
// try fetching EOL
while (true) {
// if internal buffer is empty,
// try fetching it.
if (internal_buffer.empty()) {
// console and non-console use different method to read.
if constexpr (BIsConsole) {
// console handle, use ReadConsoleW.
// read from console, the read data is wchar based
if (!ReadConsoleW(hStdIn, szReadChars, sizeof(szReadChars) / sizeof(TChar), &dwReadNumberOfChars, NULL)) break;
} else {
// anything else, use ReadFile instead.
// the read data is utf8 based
if (!ReadFile(hStdIn, szReadChars, sizeof(szReadChars), &dwReadNumberOfChars, NULL)) break;
}
// send to internal buffer
if (dwReadNumberOfChars == 0) break;
internal_buffer.append(szReadChars, dwReadNumberOfChars);
}
// try finding EOL in internal buffer
if constexpr (std::is_same_v<TChar, char>) eol_pos = internal_buffer.find_first_of('\n');
else eol_pos = internal_buffer.find_first_of(L'\n');
// check finding result
if (eol_pos == std::wstring::npos) {
// the whole string do not include EOL, fully appended to return value
return_buffer += internal_buffer;
internal_buffer.clear();
// need more data, continue while
} else {
// split result
// push into result and remain some in internal buffer.
return_buffer.append(internal_buffer, 0u, eol_pos);
internal_buffer.erase(0u, eol_pos + 1u); // +1 because EOL take one place.
// break while mean success finding
break;
}
}
// post-process for return value
std::u8string real_return_buffer;
if constexpr (BIsConsole) {
// console mode need convert wchar to utf8
auto rv = ENC::to_utf8(return_buffer);
if (rv.has_value()) real_return_buffer = std::move(rv.value());
} else {
// non-console just copt the result
real_return_buffer = REINTERPRET::as_utf8(return_buffer);
}
// every mode need delete \r words
OP::replace(real_return_buffer, u8"\r", u8"");
// return value
return real_return_buffer;
}
static void win_console_write(const std::u8string_view& strl, bool to_stderr) {
// Prepare some Win32 variables
// fetch stdout handle first
HANDLE hStdOut = GetStdHandle(to_stderr ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
DWORD dwConsoleMode;
DWORD dwWrittenNumberOfChars;
// if stdout was redirected, this handle may point to a file handle or anything else,
// WriteConsoleW can not write data into such scenario, so we need check whether this handle is console handle
if (GetConsoleMode(hStdOut, &dwConsoleMode)) {
// console handle, use WriteConsoleW.
// convert utf8 string to wide char first
auto rv = ENC::to_wchar(strl);
if (!rv.has_value()) return;
std::wstring wstrl(std::move(rv.value()));
size_t wstrl_size = wstrl.size();
// write string with size check
if (wstrl_size <= std::numeric_limits<DWORD>::max()) {
WriteConsoleW(hStdOut, wstrl.c_str(), static_cast<DWORD>(wstrl_size), &dwWrittenNumberOfChars, NULL);
}
} else {
// anything else, use WriteFile instead.
// WriteFile do not need extra convertion, because it is direct writing.
// check whether string length is overflow
size_t strl_size = strl.size() * sizeof(std::u8string_view::value_type);
// write string with size check
if (strl_size <= std::numeric_limits<DWORD>::max()) {
WriteFile(hStdOut, strl.data(), static_cast<DWORD>(strl_size), &dwWrittenNumberOfChars, NULL);
}
}
}
#endif
#pragma endregion
#pragma region Read Functions
std::u8string read_line() {
#if defined(YYCC_OS_WINDOWS)
// get stdin mode
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
// use different method to get according to whether stdin is redirected
DWORD dwConsoleMode;
if (GetConsoleMode(hStdIn, &dwConsoleMode)) {
return win_console_read<true>(hStdIn);
} else {
return win_console_read<false>(hStdIn);
}
#else
// in linux, directly use C++ function to fetch.
std::string cmd;
if (std::getline(std::cin, cmd).fail()) cmd.clear();
return REINTERPRET::as_utf8(cmd);
#endif
}
#pragma endregion
#pragma region Write Functions
template<bool BNeedFmt, bool BIsErr, bool BHasEOL>
static void raw_write(const char8_t* u8_fmt, va_list argptr) {
// Buiild string need to be written first
// If no format string or plain string for writing, return.
if (u8_fmt == nullptr) return;
// Build or simply copy string
std::u8string strl;
if constexpr (BNeedFmt) {
// treat as format string
va_list argcpy;
va_copy(argcpy, argptr);
auto rv = OP::vprintf(u8_fmt, argcpy);
va_end(argcpy);
// check format result
if (!rv.has_value()) return;
else strl = std::move(rv.value());
} else {
// treat as plain string
strl = u8_fmt;
}
// Checkout whether add EOL
if constexpr (BHasEOL) {
strl += u8'\n';
}
#if defined(YYCC_OS_WINDOWS)
// call Windows specific writer
win_console_write(strl, BIsErr);
#else
// in linux, directly use C function to write.
std::fputs(REINTERPRET::as_ordinary(strl.c_str()), BIsErr ? stderr : stdout);
#endif
}
void format(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, false, false>(u8_fmt, argptr);
va_end(argptr);
}
void format_line(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, false, true>(u8_fmt, argptr);
va_end(argptr);
}
void write(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, false, false>(u8_strl, empty);
}
void write_line(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, false, true>(u8_strl, empty);
}
void eformat(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, true, false>(u8_fmt, argptr);
va_end(argptr);
}
void eformat_line(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, true, true>(u8_fmt, argptr);
va_end(argptr);
}
void ewrite(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, true, false>(u8_strl, empty);
}
void ewrite_line(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, true, true>(u8_strl, empty);
}
#pragma endregion
}

View File

@ -0,0 +1,111 @@
#pragma once
#include <string>
/**
* @brief The helper providing universal C\# style console function and other console related stuff
* @details
* The origin of this namespace is coming from the requirement of UTF8 console in Windows.
* There are 3 ways to make Windows console enable UTF8 mode.
*
* First one is calling \c SetConsoleCP and \c SetConsoleOutputCP.
* The side effect of this is \c std::cin and \c std::cout is broken,
* however there is a patch for this issue.
*
* Second one is calling \c _set_mode with \c _O_U8TEXT or \c _O_U16TEXT to enable Unicode mode for Windows console.
* This also have side effect which is stronger than first one.
* All "puts" family functions (ASCII-based output functions) will throw assertion exception.
* You only can use "putws" family functions (wide-char-based output functions).
* However these functions can not be used without calling \c _set_mode in Windows design.
*
* There still is another method, using \c WriteConsoleW directly visiting console.
* This function family can output correct string without calling any extra functions!
* This method is what we adopted and finally become this namespace.
*
* Reference:
* \li https://stackoverflow.com/questions/45575863/how-to-print-utf-8-strings-to-stdcout-on-windows
* \li https://stackoverflow.com/questions/69830460/reading-utf-8-input
*
* For how to utilize this functions provided by this namespace, please view \ref console_helper.
*
* @warning
* All functions provided by this namespace are too aggressive.
* Once you use it, you should not use any other input output functions.
*
* @deprecated
* This namespace provided functions are too aggressive and can not cover all use scenario.
* So I start to give this up when migrating this namespace during developing YYCC 2.x version.
* I just do a simple type fix and rename when migrating this namespace to make it "just works".
* There is no suggestion for using this namespace. And there is no update for this namespace.
* Programmer should treat Windows UTF8 issue on their own.
*/
namespace yycc::carton::csconsole {
/**
* @brief Reads the next line of UTF8 characters from the standard input stream.
* @return
* The next line of UTF8 characters from the input stream.
* Empty string if user just press Enter key or function failed.
*/
std::u8string read_line();
/**
* @brief
* Writes the text representation of the specified object
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void format(const char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void format_line(const char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void write(const char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void write_line(const char8_t* u8_strl);
/**
* @brief
* Writes the text representation of the specified object
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void eformat(const char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void eformat_line(const char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ewrite(const char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ewrite_line(const char8_t* u8_strl);
}

View File

@ -152,8 +152,13 @@ namespace yycc::encoding::windows {
// 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 (true) {
bool not_tail = ptr < end;
const char* new_ptr = not_tail ? ptr : &NULL_TERMINAL;
size_t new_size = not_tail ? end - ptr : sizeof(NULL_TERMINAL);
size_t rc = std::mbrtoc16(&c16, new_ptr, new_size, &state);
if (!rc) break;
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

View File

@ -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

View File

@ -0,0 +1,23 @@
#include "stream.hpp"
#include "reinterpret.hpp"
#define REINTERPRET ::yycc::string::reinterpret
namespace yycc::string::stream {
std::ostream& operator<<(std::ostream& os, const std::u8string_view& u8str) {
os << REINTERPRET::as_ordinary_view(u8str);
return os;
}
std::ostream& operator<<(std::ostream& os, const char8_t* u8str) {
os << REINTERPRET::as_ordinary(u8str);
return os;
}
std::ostream& operator<<(std::ostream& os, char8_t u8chr) {
os << static_cast<char>(u8chr);
return os;
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <ostream>
#include <string_view>
/**
* @brief This namespace add UTF8 support for \c std::ostream.
* @details
* The operator overloads written in this namespace will give \c std::ostream ability to write UTF8 string and its char.
* For using this feature, please directly use <TT>using namespace yycc::string:stream;</TT> to import this namespace.
*/
namespace yycc::string::stream {
std::ostream& operator<<(std::ostream& os, const std::u8string_view& u8str);
std::ostream& operator<<(std::ostream& os, const char8_t* u8str);
std::ostream& operator<<(std::ostream& os, char8_t u8chr);
}

View File

@ -0,0 +1,38 @@
#include "console.hpp"
#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL)
#include <cstdio>
#include "import_guard_head.hpp"
#include <Windows.h>
#include <io.h>
#include "import_guard_tail.hpp"
namespace yycc::windows::console {
static ExecResult<void> colorful_fs(FILE* fs) {
if (!_isatty(_fileno(fs))) {
return std::unexpected(ExecError::NotTty);
}
HANDLE h_output;
DWORD dw_mode;
h_output = (HANDLE) _get_osfhandle(_fileno(fs));
if (!GetConsoleMode(h_output, &dw_mode)) {
return std::unexpected(ExecError::GetMode);
}
if (!SetConsoleMode(h_output, dw_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) {
return std::unexpected(ExecError::SetMode);
}
return {};
}
ExecResult<void> colorful_console() {
return colorful_fs(stdout).and_then([]() { return colorful_fs(stderr); });
}
} // namespace yycc::windows::console
#endif

View File

@ -0,0 +1,33 @@
#pragma once
#include "../macro/os_detector.hpp"
#include "../macro/stl_detector.hpp"
#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL)
#include <expected>
/**
* @brief The namespace provide patches for Windows console.
*/
namespace yycc::windows::console {
/// @brief Error occurs in this module.
enum class ExecError {
NotTty, ///< Given stream is not TTY.
GetMode, ///< Can not get stream mode.
SetMode, ///< Can not set stream mode.
};
/// @brief Result type used in this module.
template<typename T>
using ExecResult = std::expected<T, ExecError>;
/**
* @brief Enable console color support for Windows.
* @details This actually is enable virtual console feature for \c stdout and \c stderr.
* @return Nothing or error occurs.
*/
ExecResult<void> colorful_console();
}
#endif

View File

@ -1,344 +1,616 @@
#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 <stdexcept>
#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<const yycc_char8_t*> 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<UINT>(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[i];
auto& data_struct = m_WinDataStruct[i];
// 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<UINT>::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<std::u8string_view> 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<WinFileFilters> 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<UINT>(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
// please note that we do not check whether this list is empty in there.
// this was done in generic_file_dialog() function.
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<WinFileDialog> 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<UINT>::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<UINT>(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 can be safely casted into UINT.
auto win_def_index_base0 = SAFECAST::try_to<UINT>(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<UINT>(win_def_index_base0.value(), 1);
if (!win_def_index.has_value()) {
return std::unexpected(DialogError::IndexOverflow);
}
// 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
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.
*/
static bool ExtractDisplayName(IShellItem* item, yycc_u8string& ret) {
* @brief Extract display name from given IShellItem*.
* @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 DialogResult<std::u8string> 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.
*/
template<CommonFileDialogType EDialogType>
static bool CommonFileDialog(const FileDialog& params, std::vector<yycc_u8string>& ret) {
* @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<GenericFileDialogType EDialogType>
static DialogResult<DialogOutcome<std::vector<std::u8string>>> 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) {
// 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;
if constexpr (EDialogType != GenericFileDialogType::OpenFolder) {
// 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.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<std::u8string> 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<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::OpenFile>(params, cache);
if (isok) ret = cache.front();
return isok;
}
bool OpenMultipleFileDialog(const FileDialog& params, std::vector<yycc_u8string>& ret) {
return CommonFileDialog<CommonFileDialogType::OpenMultipleFiles>(params, ret);
}
bool SaveFileDialog(const FileDialog& params, yycc_u8string& ret) {
std::vector<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::SaveFile>(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<DialogOutcome<std::u8string>> transform_generic_rv(DialogResult<DialogOutcome<std::vector<std::u8string>>>&& 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 vec.front();
} else {
return std::nullopt;
}
} else {
return std::unexpected(rhs.error());
}
}
bool OpenFolderDialog(const FileDialog& params, yycc_u8string& ret) {
std::vector<yycc_u8string> cache;
bool isok = CommonFileDialog<CommonFileDialogType::OpenFolder>(params, cache);
if (isok) ret = cache.front();
return isok;
DialogResult<DialogOutcome<std::u8string>> open_file(const FileDialog& params){
return transform_generic_rv(generic_file_dialog<GenericFileDialogType::OpenFile>(params));
}
DialogResult<DialogOutcome<std::vector<std::u8string>>> open_files(const FileDialog& params) {
return generic_file_dialog<GenericFileDialogType::OpenFiles>(params);
}
DialogResult<DialogOutcome<std::u8string>> save_file(const FileDialog& params) {
return transform_generic_rv(generic_file_dialog<GenericFileDialogType::SaveFile>(params));
}
DialogResult<DialogOutcome<std::u8string>> open_folder(const FileDialog& params) {
return transform_generic_rv(generic_file_dialog<GenericFileDialogType::OpenFolder>(params));
}
#pragma endregion

View File

@ -6,14 +6,19 @@
#include "../macro/class_copy_move.hpp"
#include "com.hpp"
#include <string>
#include <string_view>
#include <vector>
#include <initializer_list>
#include <expected>
#include <optional>
#include "import_guard_head.hpp"
#include <Windows.h>
#include <shlobj_core.h>
#include "import_guard_tail.hpp"
#define NS_YYCC_WINDOWS_COM ::yycc::windows::com
/**
* @brief The namespace providing Windows universal dialog features.
* @details
@ -22,283 +27,291 @@
*/
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.
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.
};
/// @brief The result type used in this module.
template<typename T>
using DialogResult = std::expected<T, DialogError>;
/**
* @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.
*/
* @brief The class representing the file types region in file dialog.
* @details
* 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<UINT>(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<WinFilterName, WinFilterModes>;
std::vector<WinFilterPair> m_WinFilters;
std::unique_ptr<COMDLG_FILTERSPEC[]> 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<WinFilterPair> m_WinFilters;
std::vector<COMDLG_FILTERSPEC> m_WinDataStruct;
};
/**
* @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.
*/
* @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: <TT>Add(u8"Microsoft Word (*.doc; *.docx)", {u8"*.doc", u8"*.docx"})</TT>
*/
bool Add(const yycc_char8_t* filter_name, std::initializer_list<const yycc_char8_t*> 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<std::u8string>;
using FilterName = std::u8string;
using FilterPair = std::pair<FilterName, FilterModes>;
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 a string representing a single filter pattern.
* This list at least should have one pattern.
* @return True if added success, otherwise false.
* @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.
*/
void add_filter(const std::u8string_view& filter_name, std::initializer_list<std::u8string_view> il);
/**
* @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();
/**
* @private
* @brief Build Windows used file filters struct.
* @return Built Windows used struct, or error occurs.
*/
DialogResult<WinFileFilters> to_windows() const;
private:
std::vector<FilterPair> 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.
*/
* @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)
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;
/**
* @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.
*/
* @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();
}
std::optional<std::wstring> m_WinTitle, m_WinInitFileName;
NS_YYCC_WINDOWS_COM::SmartIShellItem m_WinInitDirectory;
};
/**
* @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.
*/
* @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)
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; }
* @brief Set the owner of dialog.
* @param[in] owner The \c HWND pointing to the owner of dialog, or NULL to remove 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.
*/
void SetTitle(const yycc_char8_t* title) {
if (m_HasTitle = title != nullptr) m_Title = title;
}
* @brief Set custom title of dialog
* @param[in] title The string pointer to custom title.
*/
void set_title(const std::u8string_view& 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 Remove custom title of dialog (keep system default)
*/
void unset_title();
/**
* @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.
*/
void set_init_file_name(const std::u8string_view& init_filename);
/**
* @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 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.
* 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 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& configre_file_types();
/**
* @brief Set the index of default selected file filter.
* @param[in] idx
* The index to default file filter.
* This must be a valid index in file filters.
*/
void set_default_file_type_index(size_t idx);
/**
* @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.
*/
bool Generate(WinFileDialog& win_result) const;
* @private
* @brief Build Windows used dialog info struct.
* @return Built Windows used struct, or error occurs.
*/
DialogResult<WinFileDialog> to_windows() const;
protected:
HWND m_Owner;
bool m_HasTitle, m_HasInitFileName, m_HasInitDirectory;
std::u8string m_Title, m_InitFileName, m_InitDirectory;
std::optional<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.
*/
* @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 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<typename T>
using DialogOutcome = std::optional<T>;
/**
* @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<std::u8string>& ret);
* @brief Open the dialog which order user select single file to open.
* @param[in] params The configuration of dialog.
* @return Full path to user selected file, or nothing (click "Cancel"), or error occurs.
*/
DialogResult<DialogOutcome<std::u8string>> open_file(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.
*/
bool SaveFileDialog(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.
* @return The list of full path of user selected files, or nothing (click "Cancel"), or error occurs.
*/
DialogResult<DialogOutcome<std::vector<std::u8string>>> open_files(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.
*/
bool OpenFolderDialog(const FileDialog& params, std::u8string& ret);
* @brief Open the dialog which order user select single file to save.
* @param[in] params The configuration of dialog.
* @return Full path to user selected file, or nothing (click "Cancel"), or error occurs.
*/
DialogResult<DialogOutcome<std::u8string>> save_file(const FileDialog& params);
/**
* @brief Open the dialog which order user select single directory to open.
* @param[in] params The configuration of dialog.
* @return Full path to user selected directory, or nothing (click "Cancel"), or error occurs.
*/
DialogResult<DialogOutcome<std::u8string>> open_folder(const FileDialog& params);
}
#undef NS_YYCC_WINDOWS_COM
#endif

View File

@ -19,8 +19,9 @@ PRIVATE
yycc/patch/ptr_pad.cpp
yycc/patch/fopen.cpp
yycc/rust/env.cpp
yycc/string/op.cpp
yycc/string/reinterpret.cpp
yycc/string/op.cpp
yycc/string/stream.cpp
yycc/num/parse.cpp
yycc/num/stringify.cpp
yycc/num/op.cpp
@ -30,8 +31,9 @@ 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/windows/console.cpp
yycc/carton/pycodec.cpp
yycc/carton/termcolor.cpp

View File

@ -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<YYCC::yycc_u8string> 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<std::wstring> 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<std::u16string> 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<std::u32string> 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<YYCC::yycc_u8string> 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<UINT>(1252)), YYCC_U8("YYCC::WinFctHelper::IsValidCodePage"));
Assert(!YYCC::WinFctHelper::IsValidCodePage(static_cast<UINT>(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<int> test_set { 1, 2, 3, 4, 6, 7 };
Assert(YYCC::StdPatch::Contains(test_set, static_cast<int>(1)), YYCC_U8("YYCC::StdPatch::Contains"));
Assert(!YYCC::StdPatch::Contains(test_set, static_cast<int>(5)), YYCC_U8("YYCC::StdPatch::Contains"));
std::map<int, float> test_map { { 1, 1.0f }, { 4, 4.0f } };
Assert(YYCC::StdPatch::Contains(test_map, static_cast<int>(1)), YYCC_U8("YYCC::StdPatch::Contains"));
Assert(!YYCC::StdPatch::Contains(test_map, static_cast<int>(5)), YYCC_U8("YYCC::StdPatch::Contains"));
}
enum class TestEnum : int8_t {
Test1, Test2, Test3
};

View File

@ -157,7 +157,7 @@ namespace yyccshared::literals {
#pragma region OtherLiterals Data
std::vector<OtherLiteral> OTHERLIT_OTHERSTR_VEC{{"\xC4\xE3\xBA\xC3\xD6\xD0\xB9\xFA", UINT32_C(936), "GBK", u8"gbk"}};
static std::vector<OtherLiteral> OTHERLIT_OTHERSTR_VEC{{"\xC4\xE3\xBA\xC3\xD6\xD0\xB9\xFA", UINT32_C(936), "GBK", u8"gbk"}};
#define OTHER_STR_GBK "\u4f60\u597d\u4e2d\u56fd"

View File

@ -1,13 +1,61 @@
#include <gtest/gtest.h>
#include <yycc.hpp>
#include <yycc/carton/tabulate.hpp>
#include <yycc/string/reinterpret.hpp>
#include <sstream>
#define TABULATE ::yycc::carton::tabulate
#define REINTERPRET ::yycc::string::reinterpret
namespace yycctest::carton::tabulate {
TEST(CartonTabulate, Main) {
class CartonTabulate : public ::testing::Test {
protected:
CartonTabulate() : table(3u), ss() {
// setup basic data
table.set_prefix(u8"# ");
table.set_header({u8"中文1", u8"中文2", u8"中文3"});
table.set_bar(u8"===");
table.add_row({u8"a", u8"b", u8"c"});
}
~CartonTabulate() override = default;
void expected_print(const std::u8string_view& exp) {
ss.str("");
table.print(ss);
EXPECT_EQ(REINTERPRET::as_utf8_view(ss.view()), exp);
}
TABULATE::Tabulate table;
std::stringstream ss;
};
TEST_F(CartonTabulate, Full) {
table.show_header(true);
table.show_bar(true);
expected_print(u8"# 中文1 中文2 中文3 \n"
u8"# === === === \n"
u8"# a b c \n");
}
}
TEST_F(CartonTabulate, NoHeader) {
table.show_header(false);
table.show_bar(true);
expected_print(u8"# === === === \n"
u8"# a b c \n");
}
TEST_F(CartonTabulate, NoBar) {
table.show_header(true);
table.show_bar(false);
expected_print(u8"# 中文1 中文2 中文3 \n"
u8"# a b c \n");
}
TEST_F(CartonTabulate, OnlyData) {
table.show_header(false);
table.show_bar(false);
expected_print(u8"# a b c \n");
}
} // namespace yycctest::carton::tabulate

View File

@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include <yycc.hpp>
#include <yycc/string/stream.hpp>
#include <yycc/string/reinterpret.hpp>
#include <sstream>
#define REINTERPRET ::yycc::string::reinterpret
using namespace std::literals::string_view_literals;
using namespace ::yycc::string::stream;
namespace yycctest::string::stream {
TEST(StringStream, StringView) {
std::stringstream ss;
ss << u8"hello"sv;
EXPECT_EQ(REINTERPRET::as_utf8_view(ss.view()), u8"hello"sv);
}
TEST(StringStream, CStrPtr) {
std::stringstream ss;
ss << u8"hello";
EXPECT_EQ(REINTERPRET::as_utf8_view(ss.view()), u8"hello");
}
TEST(StringStream, Character) {
std::stringstream ss;
ss << u8'y';
EXPECT_EQ(REINTERPRET::as_utf8_view(ss.view()), u8"y");
}
}

View File

@ -0,0 +1,17 @@
#include <gtest/gtest.h>
#include <yycc.hpp>
#include <yycc/windows/console.hpp>
#define CONSOLE ::yycc::windows::console
namespace yycctest::windows::console {
#if defined(YYCC_OS_WINDOWS) && defined(YYCC_STL_MSSTL)
TEST(WindowsConsole, ColorfulConsole) {
// Set colorful console should always be success.
auto rv = CONSOLE::colorful_console();
EXPECT_TRUE(rv.has_value());
}
#endif
}

View File

@ -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
}

View File

@ -26,9 +26,10 @@ namespace yycctest::windows::winfct {
}
TEST(WindowsWinFct, IsValidCodePage) {
// Test valid code page
EXPECT_TRUE(WINFCT::is_valid_code_page(437));
EXPECT_TRUE(WINFCT::is_valid_code_page(65001));
// This code page must be invalid
EXPECT_FALSE(WINFCT::is_valid_code_page(6161));
}