diff --git a/CHANGELOG.md b/CHANGELOG.md index 9496106cc..865a41481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,7 @@ - Dev: Cleaned up and optimized resources. (#5222) - Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) +- Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) ## 2.4.6 diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index be3ebb8ff..fe7e5ed65 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include "util/QMagicEnum.hpp" + #include #include @@ -108,10 +109,7 @@ public: template EnumStringSetting &operator=(Enum newValue) { - std::string enumName(magic_enum::enum_name(newValue)); - auto qEnumName = QString::fromStdString(enumName); - - this->setValue(qEnumName.toLower()); + this->setValue(qmagicenum::enumNameString(newValue).toLower()); return *this; } @@ -130,8 +128,8 @@ public: Enum getEnum() { - return magic_enum::enum_cast(this->getValue().toStdString(), - magic_enum::case_insensitive) + return qmagicenum::enumCast(this->getValue(), + qmagicenum::CASE_INSENSITIVE) .value_or(this->defaultValue); } diff --git a/src/common/network/NetworkPrivate.cpp b/src/common/network/NetworkPrivate.cpp index 51842dd57..ed81dd9e6 100644 --- a/src/common/network/NetworkPrivate.cpp +++ b/src/common/network/NetworkPrivate.cpp @@ -9,6 +9,7 @@ #include "util/AbandonObject.hpp" #include "util/DebugCount.hpp" #include "util/PostToThread.hpp" +#include "util/QMagicEnum.hpp" #include #include @@ -181,11 +182,9 @@ void NetworkData::emitFinally() }); } -QLatin1String NetworkData::typeString() const +QString NetworkData::typeString() const { - auto view = magic_enum::enum_name(this->requestType); - return QLatin1String{view.data(), - static_cast(view.size())}; + return qmagicenum::enumNameString(this->requestType); } void load(std::shared_ptr &&data) diff --git a/src/common/network/NetworkPrivate.hpp b/src/common/network/NetworkPrivate.hpp index 1e169a927..434d9f66d 100644 --- a/src/common/network/NetworkPrivate.hpp +++ b/src/common/network/NetworkPrivate.hpp @@ -60,7 +60,7 @@ public: void emitError(NetworkResult &&result); void emitFinally(); - QLatin1String typeString() const; + QString typeString() const; private: QString hash_; diff --git a/src/controllers/commands/builtin/twitch/Announce.cpp b/src/controllers/commands/builtin/twitch/Announce.cpp index d14d1f914..a86746195 100644 --- a/src/controllers/commands/builtin/twitch/Announce.cpp +++ b/src/controllers/commands/builtin/twitch/Announce.cpp @@ -30,11 +30,7 @@ QString sendAnnouncementColor(const CommandContext &ctx, QString colorStr = ""; if (color != HelixAnnouncementColor::Primary) { - colorStr = - QString::fromStdString( - std::string{ - magic_enum::enum_name(color)}) - .toLower(); + colorStr = qmagicenum::enumNameString(color).toLower(); } if (ctx.words.size() < 2) diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index 497c25260..d70be6489 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -119,9 +119,12 @@ int c2_register_callback(lua_State *L) return 0; } - auto callbackSavedName = QString("c2cb-%1").arg( - magic_enum::enum_name(evtType).data()); - lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str()); + auto typeName = magic_enum::enum_name(evtType); + std::string callbackSavedName; + callbackSavedName.reserve(5 + typeName.size()); + callbackSavedName += "c2cb-"; + callbackSavedName += typeName; + lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str()); lua_pop(L, 2); diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index fc0a255cc..4609fee7c 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -3,6 +3,7 @@ # include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" +# include "util/QMagicEnum.hpp" extern "C" { # include @@ -26,7 +27,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else if (!homepageObj.isUndefined()) { - QString type = magic_enum::enum_name(homepageObj.type()).data(); + auto type = qmagicenum::enumName(homepageObj.type()); this->errors.emplace_back( QString("homepage is defined but is not a string (its type is %1)") .arg(type)); @@ -38,7 +39,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(nameObj.type()).data(); + auto type = qmagicenum::enumName(nameObj.type()); this->errors.emplace_back( QString("name is not a string (its type is %1)").arg(type)); } @@ -50,7 +51,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(descrObj.type()).data(); + auto type = qmagicenum::enumName(descrObj.type()); this->errors.emplace_back( QString("description is not a string (its type is %1)").arg(type)); } @@ -64,7 +65,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = authorsArr.at(i); if (!t.isString()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back( QString("authors element #%1 is not a string (it is a %2)") .arg(i) @@ -76,7 +77,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(authorsObj.type()).data(); + auto type = qmagicenum::enumName(authorsObj.type()); this->errors.emplace_back( QString("authors is not an array (its type is %1)").arg(type)); } @@ -88,7 +89,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(licenseObj.type()).data(); + auto type = qmagicenum::enumName(licenseObj.type()); this->errors.emplace_back( QString("license is not a string (its type is %1)").arg(type)); } @@ -109,7 +110,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(verObj.type()).data(); + auto type = qmagicenum::enumName(verObj.type()); this->errors.emplace_back( QString("version is not a string (its type is %1)").arg(type)); this->version = semver::version(0, 0, 0); @@ -119,7 +120,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) { if (!permsObj.isArray()) { - QString type = magic_enum::enum_name(permsObj.type()).data(); + auto type = qmagicenum::enumName(permsObj.type()); this->errors.emplace_back( QString("permissions is not an array (its type is %1)") .arg(type)); @@ -132,7 +133,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = permsArr.at(i); if (!t.isObject()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back(QString("permissions element #%1 is not " "an object (its type is %2)") .arg(i) @@ -161,7 +162,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) { if (!tagsObj.isArray()) { - QString type = magic_enum::enum_name(tagsObj.type()).data(); + auto type = qmagicenum::enumName(tagsObj.type()); this->errors.emplace_back( QString("tags is not an array (its type is %1)").arg(type)); return; @@ -173,7 +174,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = tagsArr.at(i); if (!t.isString()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back( QString("tags element #%1 is not a string (its type is %2)") .arg(i) diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 6dfb3b20e..4450b2a01 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -107,14 +107,14 @@ public: return {}; } // this uses magic enum to help automatic tooling find usages + auto typeName = + magic_enum::enum_name(lua::api::EventType::CompletionRequested); + std::string cbName; + cbName.reserve(5 + typeName.size()); + cbName += "c2cb-"; + cbName += typeName; auto typ = - lua_getfield(this->state_, LUA_REGISTRYINDEX, - QString("c2cb-%1") - .arg(magic_enum::enum_name( - lua::api::EventType::CompletionRequested) - .data()) - .toStdString() - .c_str()); + lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str()); if (typ != LUA_TFUNCTION) { lua_pop(this->state_, 1); diff --git a/src/controllers/plugins/PluginPermission.cpp b/src/controllers/plugins/PluginPermission.cpp index d806db4bd..09204f93d 100644 --- a/src/controllers/plugins/PluginPermission.cpp +++ b/src/controllers/plugins/PluginPermission.cpp @@ -1,6 +1,8 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/PluginPermission.hpp" +# include "util/QMagicEnum.hpp" + # include # include @@ -11,14 +13,13 @@ PluginPermission::PluginPermission(const QJsonObject &obj) auto jsontype = obj.value("type"); if (!jsontype.isString()) { - QString tn = magic_enum::enum_name(jsontype.type()).data(); + auto tn = qmagicenum::enumName(jsontype.type()); this->errors.emplace_back(QString("permission type is defined but is " "not a string (its type is %1)") .arg(tn)); } - auto strtype = jsontype.toString().toStdString(); - auto opt = magic_enum::enum_cast( - strtype, magic_enum::case_insensitive); + auto opt = qmagicenum::enumCast( + jsontype.toString(), qmagicenum::CASE_INSENSITIVE); if (!opt.has_value()) { this->errors.emplace_back(QString("permission type is an unknown (%1)") diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 2b8c0ec27..82234a99c 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -6,6 +6,7 @@ #include "providers/seventv/eventapi/Message.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvCosmetics.hpp" +#include "util/QMagicEnum.hpp" #include @@ -228,7 +229,7 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) default: { qCDebug(chatterinoSeventvEventAPI) << "Unknown subscription type:" - << magic_enum::enum_name(dispatch.type).data() + << qmagicenum::enumName(dispatch.type) << "body:" << dispatch.body; } break; diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp index 03fbdac97..b4fd31044 100644 --- a/src/providers/seventv/eventapi/Dispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -1,5 +1,7 @@ #include "providers/seventv/eventapi/Dispatch.hpp" +#include "util/QMagicEnum.hpp" + #include #include @@ -7,8 +9,7 @@ namespace chatterino::seventv::eventapi { Dispatch::Dispatch(QJsonObject obj) - : type(magic_enum::enum_cast( - obj["type"].toString().toStdString()) + : type(qmagicenum::enumCast(obj["type"].toString()) .value_or(SubscriptionType::INVALID)) , body(obj["body"].toObject()) , id(this->body["id"].toString()) @@ -95,8 +96,8 @@ bool UserConnectionUpdateDispatch::validate() const CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch) : data(dispatch.body["object"]["data"].toObject()) - , kind(magic_enum::enum_cast( - dispatch.body["object"]["kind"].toString().toStdString()) + , kind(qmagicenum::enumCast( + dispatch.body["object"]["kind"].toString()) .value_or(CosmeticKind::INVALID)) { } @@ -111,8 +112,7 @@ EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch( { const auto obj = dispatch.body["object"].toObject(); this->refID = obj["ref_id"].toString(); - this->kind = magic_enum::enum_cast( - obj["kind"].toString().toStdString()) + this->kind = qmagicenum::enumCast(obj["kind"].toString()) .value_or(CosmeticKind::INVALID); const auto userConnections = obj["user"]["connections"].toArray(); diff --git a/src/providers/seventv/eventapi/Subscription.cpp b/src/providers/seventv/eventapi/Subscription.cpp index 91d330c5e..2a1a46a94 100644 --- a/src/providers/seventv/eventapi/Subscription.cpp +++ b/src/providers/seventv/eventapi/Subscription.cpp @@ -1,5 +1,7 @@ #include "providers/seventv/eventapi/Subscription.hpp" +#include "util/QMagicEnum.hpp" + #include #include #include @@ -9,14 +11,15 @@ namespace { +using namespace chatterino; using namespace chatterino::seventv::eventapi; -const char *typeToString(SubscriptionType type) +QString typeToString(SubscriptionType type) { - return magic_enum::enum_name(type).data(); + return qmagicenum::enumNameString(type); } -QJsonObject createDataJson(const char *typeName, const Condition &condition) +QJsonObject createDataJson(const QString &typeName, const Condition &condition) { QJsonObject data; data["type"] = typeName; @@ -45,7 +48,7 @@ bool Subscription::operator!=(const Subscription &rhs) const QByteArray Subscription::encodeSubscribe() const { - const auto *typeName = typeToString(this->type); + auto typeName = typeToString(this->type); QJsonObject root; root["op"] = (int)Opcode::Subscribe; root["d"] = createDataJson(typeName, this->condition); @@ -54,7 +57,7 @@ QByteArray Subscription::encodeSubscribe() const QByteArray Subscription::encodeUnsubscribe() const { - const auto *typeName = typeToString(this->type); + auto typeName = typeToString(this->type); QJsonObject root; root["op"] = (int)Opcode::Unsubscribe; root["d"] = createDataJson(typeName, this->condition); @@ -66,8 +69,7 @@ QDebug &operator<<(QDebug &dbg, const Subscription &subscription) std::visit( [&](const auto &cond) { dbg << "Subscription{ condition:" << cond - << "type:" << magic_enum::enum_name(subscription.type).data() - << '}'; + << "type:" << qmagicenum::enumName(subscription.type) << '}'; }, subscription.condition); return dbg; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 2a3b9a14e..daf17021f 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -5,6 +5,7 @@ #include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include "util/CancellationToken.hpp" +#include "util/QMagicEnum.hpp" #include #include @@ -1172,9 +1173,7 @@ void Helix::sendChatAnnouncement( QJsonObject body; body.insert("message", message); - const auto colorStr = - std::string{magic_enum::enum_name(color)}; - body.insert("color", QString::fromStdString(colorStr).toLower()); + body.insert("color", qmagicenum::enumNameString(color).toLower()); this->makePost("chat/announcements", urlQuery) .json(body) diff --git a/src/providers/twitch/pubsubmessages/AutoMod.cpp b/src/providers/twitch/pubsubmessages/AutoMod.cpp index 8c0838f6b..697db1e32 100644 --- a/src/providers/twitch/pubsubmessages/AutoMod.cpp +++ b/src/providers/twitch/pubsubmessages/AutoMod.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/AutoMod.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) @@ -7,7 +9,7 @@ PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) , data(root.value("data").toObject()) , status(this->data.value("status").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/Base.cpp b/src/providers/twitch/pubsubmessages/Base.cpp index 7bc4a2f5f..4b32786e9 100644 --- a/src/providers/twitch/pubsubmessages/Base.cpp +++ b/src/providers/twitch/pubsubmessages/Base.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/Base.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubMessage::PubSubMessage(QJsonObject _object) @@ -9,7 +11,7 @@ PubSubMessage::PubSubMessage(QJsonObject _object) , error(this->object.value("error").toString()) , typeString(this->object.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp index 8907a2d2e..244e2be98 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChannelPoints.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( @@ -7,7 +9,7 @@ PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp index 8134178c5..2cc36ca98 100644 --- a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp +++ b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( @@ -7,7 +9,7 @@ PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp index 2a7fd6f50..cac4e02fd 100644 --- a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" +#include "util/QMagicEnum.hpp" + #include #include @@ -8,8 +10,7 @@ namespace chatterino { PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) : typeString(root.value("type").toString()) { - if (const auto oType = - magic_enum::enum_cast(this->typeString.toStdString()); + if (const auto oType = qmagicenum::enumCast(this->typeString); oType.has_value()) { this->type = oType.value(); @@ -75,8 +76,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) this->updatedByUserDisplayName = updatedBy.value("display_name").toString(); this->treatmentString = data.value("treatment").toString(); - if (const auto oTreatment = magic_enum::enum_cast( - this->treatmentString.toStdString()); + if (const auto oTreatment = + qmagicenum::enumCast(this->treatmentString); oTreatment.has_value()) { this->treatment = oTreatment.value(); @@ -84,8 +85,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) this->evasionEvaluationString = data.value("ban_evasion_evaluation").toString(); - if (const auto oEvaluation = magic_enum::enum_cast( - this->evasionEvaluationString.toStdString()); + if (const auto oEvaluation = qmagicenum::enumCast( + this->evasionEvaluationString); oEvaluation.has_value()) { this->evasionEvaluation = oEvaluation.value(); @@ -93,8 +94,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) for (const auto &rType : data.value("types").toArray()) { - if (const auto oRestriction = magic_enum::enum_cast( - rType.toString().toStdString()); + if (const auto oRestriction = + qmagicenum::enumCast(rType.toString()); oRestriction.has_value()) { this->restrictionTypes.set(oRestriction.value()); diff --git a/src/providers/twitch/pubsubmessages/Whisper.cpp b/src/providers/twitch/pubsubmessages/Whisper.cpp index d0b59d0c6..2001b8ccb 100644 --- a/src/providers/twitch/pubsubmessages/Whisper.cpp +++ b/src/providers/twitch/pubsubmessages/Whisper.cpp @@ -1,11 +1,13 @@ #include "providers/twitch/pubsubmessages/Whisper.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubWhisperMessage::PubSubWhisperMessage(const QJsonObject &root) : typeString(root.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/util/QMagicEnum.hpp b/src/util/QMagicEnum.hpp new file mode 100644 index 000000000..0325102e3 --- /dev/null +++ b/src/util/QMagicEnum.hpp @@ -0,0 +1,313 @@ +#pragma once + +#include +#include +#include + +namespace chatterino::qmagicenum::detail { + +template +struct EnableIfEnum { +}; + +template +struct EnableIfEnum { + using type = R; +}; + +template , + typename D = std::decay_t> +using enable_if_t = typename EnableIfEnum< + std::is_enum_v && + std::is_invocable_r_v, + R>::type; + +template +consteval QStringView fromArray(const std::array &arr) +{ + return QStringView{arr.data(), static_cast(N - 1)}; +} + +// Only the latin1 subset may be used right now, since it's easily convertible +template +consteval bool isLatin1(std::string_view maybe) +{ + for (std::size_t i = 0; i < N; i++) + { + if (maybe[i] < 0x20 || maybe[i] > 0x7e) + { + return false; + } + } + return true; +} + +template +inline constexpr bool eq( + QStringView a, QStringView b, + [[maybe_unused]] BinaryPredicate && + p) noexcept(magic_enum::detail::is_nothrow_invocable()) +{ + // Note: operator== isn't constexpr + if (a.size() != b.size()) + { + return false; + } + + for (QStringView::size_type i = 0; i < a.size(); i++) + { + if (!p(a[i], b[i])) + { + return false; + } + } + + return true; +} + +template +consteval auto enumNameStorage() +{ + constexpr auto utf8 = magic_enum::enum_name(); + + static_assert(isLatin1(utf8), + "Can't convert non-latin1 UTF8 to UTF16"); + + std::array storage; + for (std::size_t i = 0; i < utf8.size(); i++) + { + storage[i] = static_cast(utf8[i]); + } + storage[utf8.size()] = 0; + return storage; +} + +template +inline constexpr auto ENUM_NAME_STORAGE = enumNameStorage(); + +template +consteval auto namesStorage(std::index_sequence /*unused*/) +{ + return std::array{{detail::fromArray( + ENUM_NAME_STORAGE()[I]>)...}}; +} + +template > +inline constexpr auto NAMES_STORAGE = namesStorage( + std::make_index_sequence()>{}); + +template > +using NamesStorage = decltype((NAMES_STORAGE)); + +template > +class CaseInsensitive +{ + static constexpr QChar toLower(QChar c) noexcept + { + return (c >= u'A' && c <= u'Z') + ? QChar(c.unicode() + static_cast(u'a' - u'A')) + : c; + } + +public: + template + constexpr std::enable_if_t, QChar> && + std::is_same_v, QChar>, + bool> + operator()(L lhs, R rhs) const noexcept + { + return Op{}(toLower(lhs), toLower(rhs)); + } +}; + +} // namespace chatterino::qmagicenum::detail + +namespace chatterino::qmagicenum { + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string view +template +[[nodiscard]] consteval detail::enable_if_t + enumName() noexcept +{ + return QStringView{ + detail::fromArray(detail::ENUM_NAME_STORAGE)}; +} + +/// @brief Get the name of an enum value +/// +/// @param value The enum value +/// @returns The name as a string view. If @a value does not have name or the +/// value is out of range an empty string is returned. +template > +[[nodiscard]] constexpr detail::enable_if_t enumName( + E value) noexcept +{ + using D = std::decay_t; + + if (const auto i = magic_enum::enum_index(value)) + { + return detail::NAMES_STORAGE[*i]; + } + return {}; +} + +/// @brief Gets a static QString from @a view. +/// +/// @pre @a view must be a static string view (i.e. it must be valid throughout +/// the entire duration of the program). +/// +/// @param view The view to turn into a static string +/// @returns Qt6: A static string (never gets freed), Qt5: regular string +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +[[nodiscard]] inline QString staticString(QStringView view) noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return QString(QStringPrivate(nullptr, const_cast(view.utf16()), + view.size())); +} +#else +[[nodiscard]] inline QString staticString(QStringView view) +{ + return view.toString(); +} +#endif + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. The returned string is static. +template +[[nodiscard]] inline detail::enable_if_t + enumNameString() noexcept +{ + return staticString(enumName()); +} + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. If @a value does not have name or the +/// value is out of range an empty string is returned. +/// The returned string is static. +template > +[[nodiscard]] inline detail::enable_if_t enumNameString( + E value) noexcept +{ + using D = std::decay_t; + + return staticString(enumName(value)); +} + +/// @brief Gets the enum value from a name +/// +/// @tparam E The enum type to parse the @a name as +/// @param name The name of the enum value to parse +/// @param p A predicate to compare characters of a string +/// (defaults to std::equal_to) +/// @returns A `std::optional` of the parsed value. If no value was parsed, +/// `std::nullopt` is returned. +template , + typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr detail::enable_if_t>, + BinaryPredicate> + enumCast(QStringView name, + [[maybe_unused]] BinaryPredicate p = + {}) noexcept(magic_enum::detail:: + is_nothrow_invocable()) +{ + using D = std::decay_t; + + if constexpr (magic_enum::enum_count() == 0) + { + static_cast(name); + return std::nullopt; // Empty enum. + } + + for (std::size_t i = 0; i < magic_enum::enum_count(); i++) + { + if (detail::eq(name, detail::NAMES_STORAGE[i], p)) + { + return magic_enum::enum_value(i); + } + } + return std::nullopt; // Invalid value or out of range. +} + +/// @brief Constructs a name from the @a flags +/// +/// @param flags The combined flags to construct the name from +/// @param sep A separator between each flag (defaults to u'|') +/// @returns A string containing all names separated by @a sep. If any flag in +/// @a flags is out of rage or does not have a name, an empty string +/// is returned. +template +[[nodiscard]] inline detail::enable_if_t enumFlagsName( + E flags, char16_t sep = u'|') +{ + using D = std::decay_t; + using U = std::underlying_type_t; + constexpr auto S = magic_enum::detail::enum_subtype::flags; // NOLINT + + QString name; + auto checkValue = U{0}; + for (std::size_t i = 0; i < magic_enum::enum_count(); ++i) + { + const auto v = static_cast(magic_enum::enum_value(i)); + if ((static_cast(flags) & v) != 0) + { + const auto n = detail::NAMES_STORAGE[i]; + if (!n.empty()) + { + checkValue |= v; + if (!name.isEmpty()) + { + name.append(sep); + } + name.append(n); + } + else + { + return {}; // Value out of range. + } + } + } + + if (checkValue != 0 && checkValue == static_cast(flags)) + { + return name; + } + return {}; // Invalid value or out of range. +} + +/// @brief Get the names of all values from @a E. +/// +/// @tparam E The enum type +/// @returns A `std::array` of all names (`QStringView`s) +template > +[[nodiscard]] constexpr auto enumNames() noexcept + -> detail::enable_if_t> +{ + return detail::NAMES_STORAGE, S>; +} + +/// Allows you to write qmagicenum::enumCast("bar", qmagicenum::CASE_INSENSITIVE) +inline constexpr auto CASE_INSENSITIVE = detail::CaseInsensitive<>{}; + +} // namespace chatterino::qmagicenum diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3cf64e116..1454b4999 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -33,6 +33,7 @@ #include "util/DistanceBetweenPoints.hpp" #include "util/Helpers.hpp" #include "util/IncognitoBrowser.hpp" +#include "util/QMagicEnum.hpp" #include "util/Twitch.hpp" #include "widgets/dialogs/ReplyThreadPopup.hpp" #include "widgets/dialogs/SettingsDialog.hpp" @@ -266,8 +267,7 @@ void addHiddenContextMenuItems(QMenu *menu, jsonObject["id"] = message->id; jsonObject["searchText"] = message->searchText; jsonObject["messageText"] = message->messageText; - jsonObject["flags"] = QString::fromStdString( - magic_enum::enum_flags_name(message->flags.value())); + jsonObject["flags"] = qmagicenum::enumFlagsName(message->flags.value()); jsonDocument.setObject(jsonObject); diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 50136dcee..74746be8a 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1243,7 +1243,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) helixTimegateModerators->minimumSizeHint().width()); layout.addDropdownEnumClass( - "Chat send protocol", magic_enum::enum_names(), + "Chat send protocol", qmagicenum::enumNames(), s.chatSendProtocol, "'Helix' will use Twitch's Helix API to send message. 'IRC' will use " "IRC to send messages.", @@ -1256,7 +1256,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) auto *soundBackend = layout.addDropdownEnumClass( "Sound backend (requires restart)", - magic_enum::enum_names(), s.soundBackend, + qmagicenum::enumNames(), s.soundBackend, "Change this only if you're noticing issues with sound playback on " "your system", {}); diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index d2a0a27e1..7e1625a70 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -276,7 +276,7 @@ public: template ComboBox *addDropdownEnumClass(const QString &text, - const std::array &items, + const std::array &items, EnumStringSetting &setting, QString toolTipText, const QString &defaultValueText) @@ -285,7 +285,7 @@ public: for (const auto &item : items) { - combo->addItem(QString::fromStdString(std::string(item))); + combo->addItem(item.toString()); } if (!defaultValueText.isEmpty()) @@ -296,8 +296,7 @@ public: setting.connect( [&setting, combo](const QString &value) { auto enumValue = - magic_enum::enum_cast(value.toStdString(), - magic_enum::case_insensitive) + qmagicenum::enumCast(value, qmagicenum::CASE_INSENSITIVE) .value_or(setting.defaultValue); auto i = magic_enum::enum_integer(enumValue); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f1f80cf0e..fb5730048 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp # Add your new file above this line! ) diff --git a/tests/src/QMagicEnum.cpp b/tests/src/QMagicEnum.cpp new file mode 100644 index 000000000..80c265efe --- /dev/null +++ b/tests/src/QMagicEnum.cpp @@ -0,0 +1,198 @@ +#include "util/QMagicEnum.hpp" + +#include "common/FlagsEnum.hpp" +#include "common/Literals.hpp" + +#include + +using namespace chatterino; +using namespace literals; + +using qmagicenum::enumCast; +using qmagicenum::enumFlagsName; +using qmagicenum::enumName; +using qmagicenum::enumNames; +using qmagicenum::enumNameString; + +namespace { + +enum class MyEnum { + Foo, + Bar, + Baz, +}; + +enum class MyFlag { + None = 0, + One = 1, + Two = 2, + Four = 4, + Eight = 8, +}; +using MyFlags = chatterino::FlagsEnum; + +enum class MyCustom { + Default = 1, + First = 4, + Second = 9, +}; + +enum MyOpen { + OpenOne = 11, + OpenTwo = 12, + OpenThree = 13, +}; + +consteval bool eq(QStringView a, QStringView b) +{ + return qmagicenum::detail::eq(a, b, std::equal_to<>()); +} + +template +consteval bool checkConst(E value, QStringView expectedName) +{ + return eq(enumName(value), expectedName) && + enumCast(expectedName) == value; +} + +template +consteval bool checkInsensitive(E value, QStringView possible) +{ + return enumCast(possible, qmagicenum::CASE_INSENSITIVE) == value; +} + +template ().size()> +consteval bool checkValues(std::array values) +{ + constexpr auto got = enumNames(); + if (got.size() != N) + { + return false; + } + for (size_t i = 0; i < N; i++) + { + if (!eq(got.at(i), values.at(i))) + { + return false; + } + } + return true; +} + +} // namespace + +template <> +struct magic_enum::customize::enum_range { + static constexpr bool is_flags = true; // NOLINT +}; + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name(MyCustom value) noexcept +{ + switch (value) + { + case MyCustom::First: + return "myfirst"; + case MyCustom::Second: + return "mysecond.*"; + + default: + return default_tag; + } +} + +TEST(QMagicEnum, basic) +{ + static_assert(eq(enumName(), u"Foo")); + static_assert(eq(enumName(), u"Bar")); + static_assert(eq(enumName(), u"Baz")); + static_assert(checkConst(MyEnum::Foo, u"Foo")); + static_assert(checkConst(MyEnum::Bar, u"Bar")); + static_assert(checkConst(MyEnum::Baz, u"Baz")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"Foo", u"Bar", u"Baz"})); +} + +TEST(QMagicEnum, flags) +{ + static_assert(eq(enumName(), u"None")); + static_assert(eq(enumName(), u"One")); + static_assert(eq(enumName(), u"Two")); + static_assert(eq(enumName(), u"Four")); + static_assert(eq(enumName(), u"Eight")); + + static_assert(!magic_enum::enum_index(MyFlag::None).has_value()); + static_assert(eq(enumName(MyFlag::None), u"")); + + static_assert(checkConst(MyFlag::One, u"One")); + static_assert(checkConst(MyFlag::Two, u"Two")); + static_assert(checkConst(MyFlag::Four, u"Four")); + static_assert(checkConst(MyFlag::Eight, u"Eight")); + static_assert(checkConst(MyFlag::Eight, u"Eight")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"One", u"Two", u"Four", u"Eight"})); +} + +TEST(QMagicEnum, enumNameString) +{ + ASSERT_EQ(enumNameString(), u"Baz"); + + ASSERT_EQ(enumNameString(), u"None"); + ASSERT_EQ(enumNameString(), u"Four"); + + ASSERT_EQ(enumNameString(MyEnum::Bar), u"Bar"); + ASSERT_EQ(enumNameString(MyFlag::None), u""); + ASSERT_EQ(enumNameString(MyFlag::One), u"One"); + ASSERT_EQ(enumNameString(MyCustom::Second), u"mysecond.*"); + ASSERT_EQ(enumNameString(OpenTwo), u"OpenTwo"); +} + +TEST(QMagicEnum, enumFlagsName) +{ + ASSERT_EQ(enumFlagsName(MyFlag::Eight), u"Eight"_s); + ASSERT_EQ(enumFlagsName(MyFlag::None), u""_s); + ASSERT_EQ(enumFlagsName(MyFlags{MyFlag::Eight, MyFlag::Four}.value(), u'+'), + u"Four+Eight"_s); + ASSERT_EQ(enumFlagsName( + MyFlags{MyFlag::Eight, MyFlag::One, MyFlag::Two, MyFlag::Four} + .value()), + u"One|Two|Four|Eight"_s); + ASSERT_EQ( + enumFlagsName(MyFlags{MyFlag::One, static_cast(16)}.value()), + u""_s); +} + +TEST(QMagicEnum, renamed) +{ + static_assert(eq(enumName(), u"Default")); + static_assert(eq(enumName(), u"myfirst")); + static_assert(eq(enumName(), u"mysecond.*")); + static_assert(checkConst(MyCustom::Default, u"Default")); + static_assert(checkConst(MyCustom::First, u"myfirst")); + static_assert(checkConst(MyCustom::Second, u"mysecond.*")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert( + checkValues({u"Default", u"myfirst", u"mysecond.*"})); +} + +TEST(QMagicEnum, open) +{ + static_assert(eq(enumName(), u"OpenOne")); + static_assert(eq(enumName(), u"OpenTwo")); + static_assert(eq(enumName(), u"OpenThree")); + static_assert(checkConst(OpenOne, u"OpenOne")); + static_assert(checkConst(OpenTwo, u"OpenTwo")); + static_assert(checkConst(OpenThree, u"OpenThree")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"OpenOne", u"OpenTwo", u"OpenThree"})); +} + +TEST(QMagicEnum, caseInsensitive) +{ + static_assert(checkInsensitive(MyEnum::Foo, u"foo")); + static_assert(checkInsensitive(MyEnum::Bar, u"BAR")); + static_assert(checkInsensitive(MyFlag::Four, u"fOUR")); + static_assert(checkInsensitive(MyCustom::Second, u"MySecond.*")); + static_assert(checkInsensitive(OpenOne, u"openone")); +}