diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 629d3e8..daa2a96 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -51,9 +51,9 @@ jobs: with: pkgs: boost-json cpr gettext-libintl glib gtest libsecret maddy triplet: ${{ matrix.variant.triplet }} - revision: 3c81ed09705008f13bf76f39853507bef51f71a1 + revision: 4103f46cb1ebb69ef5511cab840ecea0f441fbd7 token: ${{ github.token }} - cache-key: ${{ matrix.variant.triplet }} + cache-key: ${{ matrix.variant.triplet }}-4103f46cb1ebb69ef5511cab840ecea0f441fbd7 - name: "Build" working-directory: ${{github.workspace}}/build run: | diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index b64ba64..277eba8 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -31,9 +31,9 @@ jobs: with: pkgs: boost-json cpr gettext-libintl glib gtest maddy triplet: arm64-osx - revision: 3c81ed09705008f13bf76f39853507bef51f71a1 + revision: 4103f46cb1ebb69ef5511cab840ecea0f441fbd7 token: ${{ github.token }} - cache-key: "arm64-osx" + cache-key: "arm64-osx-4103f46cb1ebb69ef5511cab840ecea0f441fbd7" - name: "Build" working-directory: ${{github.workspace}}/build run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b0aa801..ef3cd0f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -38,9 +38,9 @@ jobs: with: pkgs: boost-json cpr gettext-libintl gtest maddy sqlcipher triplet: ${{ matrix.variant.triplet }} - revision: 3c81ed09705008f13bf76f39853507bef51f71a1 + revision: 4103f46cb1ebb69ef5511cab840ecea0f441fbd7 token: ${{ github.token }} - cache-key: ${{ matrix.variant.triplet }} + cache-key: ${{ matrix.variant.triplet }}-4103f46cb1ebb69ef5511cab840ecea0f441fbd7 - name: "Build" working-directory: ${{github.workspace}}/build run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index f242b2c..201e991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2025.7.4 +### Breaking Changes +- `maddy` dependency requires >= 1.6.0 +### New APIs +#### App +- Added `CancellationToken` class +### Fixes +#### Localization +- Fixed included headers +- Fixed Gettext::changeLanguage("C") not turning off translations + ## 2025.7.3 ### Breaking Changes None diff --git a/CMakeLists.txt b/CMakeLists.txt index 06e185d..bcd3830 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ endif() set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") #libnick Definition -project ("libnick" LANGUAGES C CXX VERSION 2025.7.3 DESCRIPTION "A cross-platform base for native Nickvision applications.") +project ("libnick" LANGUAGES C CXX VERSION 2025.7.4 DESCRIPTION "A cross-platform base for native Nickvision applications.") include(CMakePackageConfigHelpers) include(GNUInstallDirs) include(CTest) @@ -36,6 +36,7 @@ if(NOT WIN32) endif() add_library (${PROJECT_NAME} "include/app/appinfo.h" + "include/app/cancellationtoken.h" "include/app/datafilebase.h" "include/app/datafilemanager.h" "include/app/windowgeometry.h" @@ -95,6 +96,7 @@ add_library (${PROJECT_NAME} "include/update/version.h" "include/update/versiontype.h" "src/app/appinfo.cpp" + "src/app/cancellationtoken.cpp" "src/app/datafilebase.cpp" "src/app/datafilemanager.cpp" "src/app/windowgeometry.cpp" @@ -146,11 +148,8 @@ endif() find_package(Boost REQUIRED COMPONENTS json) find_package(cpr CONFIG REQUIRED) find_package(Intl REQUIRED) -target_link_libraries(${PROJECT_NAME} PUBLIC Boost::json cpr::cpr Intl::Intl) -if(USING_VCPKG) - find_package(unofficial-maddy CONFIG REQUIRED) - target_link_libraries(${PROJECT_NAME} PRIVATE unofficial::maddy::maddy) -endif() +find_package(maddy CONFIG REQUIRED) +target_link_libraries(${PROJECT_NAME} PUBLIC Boost::json cpr::cpr Intl::Intl maddy::maddy) if(WIN32) find_package(sqlcipher CONFIG REQUIRED) target_link_libraries(${PROJECT_NAME} PUBLIC sqlcipher::sqlcipher Advapi32 Dnsapi Dwmapi Gdiplus Kernel32 Shell32 UxTheme Ws2_32) diff --git a/Doxyfile b/Doxyfile index 9b0d8e6..a7edc3c 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "libnick" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "2025.7.3" +PROJECT_NUMBER = "2025.7.4" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in index 4d98257..38d2784 100644 --- a/cmake/config.cmake.in +++ b/cmake/config.cmake.in @@ -17,9 +17,7 @@ endif() find_dependency(Boost REQUIRED COMPONENTS json) find_dependency(cpr CONFIG REQUIRED) find_dependency(Intl REQUIRED) -if(USING_VCPKG) - find_dependency(unofficial-maddy REQUIRED) -endif() +find_dependency(maddy CONFIG REQUIRED) if(WIN32) find_dependency(sqlcipher CONFIG REQUIRED) elseif(APPLE) diff --git a/include/app/cancellationtoken.h b/include/app/cancellationtoken.h new file mode 100644 index 0000000..bec84d2 --- /dev/null +++ b/include/app/cancellationtoken.h @@ -0,0 +1,52 @@ +#ifndef CANCELLATIONTOKEN_H +#define CANCELLATIONTOKEN_H + +#include +#include + +namespace Nickvision::App +{ + /** + * @brief A token that can be used to cancel an operation. + */ + class CancellationToken + { + public: + /** + * @brief Constructs a CancellationToken. + * @param cancelFunction A callback function to call when the token is cancelled + */ + CancellationToken(const std::function& cancelFunction = {}); + /** + * @brief Gets whether or not the token is cancelled. + * @return True if token is cancelled, else false + */ + bool isCancelled() const; + /** + * @brief Gets the cancel function to be called when the token is cancelled. + * @return The cancel function + */ + const std::function& getCancelFunction() const; + /** + * @brief Sets the cancel function to be called when the token is cancelled. + * @param cancelFunction The cancel function + */ + void setCancelFunction(const std::function& cancelFunction); + /** + * @brief Cancels the token. + */ + void cancel(); + /** + * @brief Converts the token to a boolean. + * @return True if token is cancelled, else false + */ + operator bool() const; + + private: + mutable std::mutex m_mutex; + bool m_cancelled; + std::function m_cancelFunction; + }; +} + +#endif //CANCELLATIONTOKEN_H \ No newline at end of file diff --git a/include/events/event.h b/include/events/event.h index e7ed0a7..7cdc0f5 100644 --- a/include/events/event.h +++ b/include/events/event.h @@ -112,7 +112,10 @@ namespace Nickvision::Events std::lock_guard lock{ m_mutex }; for (const std::function& handler : m_handlers) { - handler(param); + if(handler) + { + handler(param); + } } } /** diff --git a/include/localization/gettext.h b/include/localization/gettext.h index 7219f18..a7238f1 100644 --- a/include/localization/gettext.h +++ b/include/localization/gettext.h @@ -29,8 +29,8 @@ #include #define GETTEXT_CONTEXT_SEPARATOR "\004" -#define _(String) dgettext(::Nickvision::Localization::Gettext::getDomainName().c_str(), String) -#define _n(String, StringPlural, N) dngettext(::Nickvision::Localization::Gettext::getDomainName().c_str(), String, StringPlural, static_cast(N)) +#define _(String) ::Nickvision::Localization::Gettext::dgettext(String) +#define _n(String, StringPlural, N) ::Nickvision::Localization::Gettext::dngettext(String, StringPlural, static_cast(N)) #define _f(String, ...) ::Nickvision::Localization::Gettext::fgettext(String, __VA_ARGS__) #define _fn(String, StringPlural, N, ...) ::Nickvision::Localization::Gettext::fngettext(String, StringPlural, static_cast(N), __VA_ARGS__) #define _p(Context, String) ::Nickvision::Localization::Gettext::pgettext(Context GETTEXT_CONTEXT_SEPARATOR String, String) @@ -57,19 +57,34 @@ namespace Nickvision::Localization::Gettext const std::vector& getAvailableLanguages(); /** * @brief Changes the current language for gettext translations. - * @param language The language code to change translations to (use "C" to turn off translations and use the default language) + * @param language The language code to change translations to (use "C" to turn off translations; use "" to use the system default language) * @return True if the language was changed successfully, else false */ bool changeLanguage(const std::string& language); + /** + * @brief Translates a message. + * @param msgid The message to translate + * @return The translated message + */ + const char* dgettext(const char* msgid); + /** + * @brief Translates a plural message. + * @param msg The message to translate + * @param msgPlural The plural version of the message to translate + * @param n The number of objects (used to determine whether or not to use the plural version of the message) + * @return The translated message for the given number of objects + */ + const char* dngettext(const char* msg, const char* msgPlural, unsigned long n); /** * @brief Translates a message and formats it with the given arguments. * @param msg The message to translate * @param args The arguments to format the translated message with + * @return The formatted translated message */ template std::string fgettext(const char* msg, Args&&... args) { - return std::vformat(_(msg), std::make_format_args(args...)); + return std::vformat(Nickvision::Localization::Gettext::dgettext(msg), std::make_format_args(args...)); } /** * @brief Translates a plural message and formats it with the given arguments. @@ -77,11 +92,12 @@ namespace Nickvision::Localization::Gettext * @param msgPlural The plural version of the message to translate * @param n The number of objects (used to determine whether or not to use the plural version of the message) * @param args The arguments to format the translated message with + * @return The formatted translated message for the given number of objects */ template std::string fngettext(const char* msg, const char* msgPlural, unsigned long n, Args&&... args) { - return std::vformat(_n(msg, msgPlural, n), std::make_format_args(args...)); + return std::vformat(Nickvision::Localization::Gettext::dngettext(msg, msgPlural, n), std::make_format_args(args...)); } /** * @brief Translates a message for a given context. diff --git a/manual/README.md b/manual/README.md index c756c4f..66ad4b9 100644 --- a/manual/README.md +++ b/manual/README.md @@ -6,15 +6,16 @@ libnick provides Nickvision apps with a common set of cross-platform APIs for managing system and desktop app functionality such as network management, taskbar icons, translations, app updates, and more. -## 2025.7.3 +## 2025.7.4 ### Breaking Changes -None +- `maddy` dependency requires >= 1.6.0 ### New APIs -#### Localization -- You can now specify "C" in the `Gettext::changeLanguage()` function to turn off translations. +#### App +- Added `CancellationToken` class ### Fixes #### Localization -- Improved `Gettext::getAvailableLanguages()`'s search for languages +- Fixed included headers +- Fixed Gettext::changeLanguage("C") not turning off translations ## Dependencies The following are a list of dependencies used by libnick. diff --git a/src/app/cancellationtoken.cpp b/src/app/cancellationtoken.cpp new file mode 100644 index 0000000..78fc274 --- /dev/null +++ b/src/app/cancellationtoken.cpp @@ -0,0 +1,50 @@ +#include "app/cancellationtoken.h" + +namespace Nickvision::App +{ + CancellationToken::CancellationToken(const std::function& cancelFunction) + : m_cancelled{ false }, + m_cancelFunction{ cancelFunction } + { + + } + + bool CancellationToken::isCancelled() const + { + std::lock_guard lock{ m_mutex }; + return m_cancelled; + } + + const std::function& CancellationToken::getCancelFunction() const + { + std::lock_guard lock{ m_mutex }; + return m_cancelFunction; + } + + void CancellationToken::setCancelFunction(const std::function& cancelFunction) + { + std::lock_guard lock{ m_mutex }; + m_cancelFunction = cancelFunction; + } + + void CancellationToken::cancel() + { + std::unique_lock lock{ m_mutex }; + if(m_cancelled) + { + return; + } + m_cancelled = true; + if(m_cancelFunction) + { + lock.unlock(); + m_cancelFunction(); + } + } + + CancellationToken::operator bool() const + { + std::lock_guard lock{ m_mutex }; + return m_cancelled; + } +} \ No newline at end of file diff --git a/src/localization/gettext.cpp b/src/localization/gettext.cpp index 2e1c163..ef2bebe 100644 --- a/src/localization/gettext.cpp +++ b/src/localization/gettext.cpp @@ -10,7 +10,8 @@ using namespace Nickvision::System; namespace Nickvision::Localization { - static std::string s_domainName; + static std::string s_domainName{}; + static bool s_translationsOff{ false }; bool Gettext::init(const std::string& domainName) { @@ -57,10 +58,18 @@ namespace Nickvision::Localization bool Gettext::changeLanguage(const std::string& language) { - if(language == "C") + if(language.empty()) { - Environment::setVariable("LANGUAGE", ""); - setlocale(LC_ALL, "C"); + if(Environment::hasVariable("LANGUAGE")) + { + Environment::clearVariable("LANGUAGE"); + } + s_translationsOff = false; + return true; + } + else if(language == "C") + { + s_translationsOff = true; return true; } const std::vector& langs{ Gettext::getAvailableLanguages() }; @@ -69,12 +78,35 @@ namespace Nickvision::Localization return false; } Environment::setVariable("LANGUAGE", language); + s_translationsOff = false; return true; } + const char* Gettext::dgettext(const char* msgid) + { + if(s_translationsOff) + { + return msgid; + } + return ::dgettext(s_domainName.c_str(), msgid); + } + + const char* Gettext::dngettext(const char* msg, const char* msgPlural, unsigned long n) + { + if(s_translationsOff) + { + return n == 1 ? msg : msgPlural; + } + return ::dngettext(s_domainName.c_str(), msg, msgPlural, n); + } + const char* Gettext::pgettext(const char* context, const char* msg) { - const char* translation{ dcgettext(s_domainName.c_str(), context, LC_MESSAGES) }; + if(s_translationsOff) + { + return msg; + } + const char* translation{ ::dcgettext(s_domainName.c_str(), context, LC_MESSAGES) }; if (translation == context) { return msg; @@ -84,7 +116,11 @@ namespace Nickvision::Localization const char* Gettext::pngettext(const char* context, const char* msg, const char* msgPlural, unsigned long n) { - const char* translation{ dcngettext(s_domainName.c_str(), context, msgPlural, n, LC_MESSAGES) }; + if(s_translationsOff) + { + return n == 1 ? msg : msgPlural; + } + const char* translation{ ::dcngettext(s_domainName.c_str(), context, msgPlural, n, LC_MESSAGES) }; if (translation == context || translation == msgPlural) { return n == 1 ? msg : msgPlural;