diff --git a/CHANGELOG.md b/CHANGELOG.md index dfaefbd6..27b1d827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Dev: Added a `run-and-kill.sh` script to help debug crash-on-exit bugs. (#6188) - Dev: Refactored the `TimeoutStackStyle` enum into its own file. (#6216) - Dev: Refactored `Notebook`-related enums into their own file. (#6220) +- Dev: Implemented customizable display names for enums. (#6238) - Dev: Refactored event API initialization away from Application and into TwitchIrcServer. (#6198) - Dev: Updated GoogleTest to v1.17.0. (#6180) - Dev: Mini refactor of `TwitchAccount`. (#6182) diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 467b9b47..95716b96 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -97,7 +97,7 @@ enum class TabStyle : std::uint8_t { Compact, }; -/// Settings which are availlable for reading and writing on the gui thread. +/// Settings which are available for reading and writing on the gui thread. // These settings are still accessed concurrently in the code but it is bad practice. class Settings { @@ -294,8 +294,6 @@ public: "/behaviour/autoSubToParticipatedThreads", true, }; - // BoolSetting twitchSeperateWriteConnection = - // {"/behaviour/twitchSeperateWriteConnection", false}; // Auto-completion BoolSetting onlyFetchChattersForSmallerStreamers = { diff --git a/src/util/QMagicEnumTagged.hpp b/src/util/QMagicEnumTagged.hpp new file mode 100644 index 00000000..d76fcbd2 --- /dev/null +++ b/src/util/QMagicEnumTagged.hpp @@ -0,0 +1,209 @@ +#pragma once + +#include "util/QMagicEnum.hpp" + +#include + +namespace chatterino::qmagicenum { + +using customize_t = std::optional; + +namespace detail { + +namespace tag { + +struct DisplayName { +}; + +} // namespace tag + +/// Builds the tagged data for a specific enum value (V), type (E), and Tag. +/// +/// If the Tag is DisplayName, attempts to obtain a custom display name for the enum value V by calling +/// qmagicenumDisplayName(V). If no customization is provided, falls back to magic_enum's name for the value. +/// +/// @tparam E Enum type +/// @tparam Tag Tag type (e.g. DisplayName) +/// @tparam V Enum value +/// @returns A compile-time static string containing the tagged data for the enum value +template +constexpr auto buildEnumValueTaggedData() noexcept +{ + [[maybe_unused]] constexpr auto custom = [] { + static_assert( + std::is_same_v, + "unhandled tag in QMagicEnumTagged.hpp::enumTaggedDataValue"); + + if constexpr (std::is_same_v) + { + return qmagicenumDisplayName(V); + } + }(); + + if constexpr (custom.has_value()) + { + constexpr auto name = *custom; + static_assert(!name.empty(), + "qmagicenum::customize must return a non-empty string."); + return magic_enum::detail::static_str{name}; + } + else + { + // No specialization for this enum value; fall back to magic_enum's value name + return magic_enum::detail::enum_name_v; + } +} + +/// Stores a static std::string_view for each (enum value, Tag) pair. +/// +/// For example, for MyEnum { Foo, Bar } and DisplayName tag, this will generate: +/// TAGGED_DATA_STORAGE_MyEnum_DisplayName_Foo = "Custom display name for Foo" +/// TAGGED_DATA_STORAGE_MyEnum_DisplayName_Bar = "Custom display name for Bar" +template +inline constexpr auto TAGGED_DATA_STORAGE = + buildEnumValueTaggedData(); + +/// Converts the static string representation of an enum value + tag into a std::array +template +consteval auto enumTaggedDataStorage() +{ + constexpr std::string_view utf8 = TAGGED_DATA_STORAGE; + + 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; +} + +/// Stores a std::array for each (enum value, Tag) pair. +/// +/// For example, for MyEnum { Foo, Bar } and DisplayName tag, this will generate: +/// TAGGED_DATA_MyEnum_DisplayName_Foo = "Custom display name for Foo" (char16_t array) +/// TAGGED_DATA_MyEnum_DisplayName_Bar = "Custom display name for Bar" (char16_t array) +template +inline constexpr auto TAGGED_DATA = + enumTaggedDataStorage(); + +/// Builds a std::array for an enum type and tag, holding the tagged data for each enum value. +/// +/// For example, for MyEnum { Foo, Bar } and DisplayName tag, this will generate: +/// INDEXED_DATA_STORAGE_MyEnum_DisplayName[0] = "Custom display name for Foo" +/// INDEXED_DATA_STORAGE_MyEnum_DisplayName[1] = "Custom display name for Bar" +template +consteval auto taggedDataStorage(std::index_sequence /*unused*/) +{ + return std::array{{detail::fromArray( + TAGGED_DATA()[I]>)...}}; +} + +/// Stores a std::array for a given enum type and tag, allowing indexed access to tagged data. +/// +/// For example, for MyEnum { Foo, Bar } and DisplayName tag, this will generate: +/// INDEXED_TAGGED_DATA_MyEnum_DisplayName[0] = "Custom display name for Foo" +/// INDEXED_TAGGED_DATA_MyEnum_DisplayName[1] = "Custom display name for Bar" +template +inline constexpr auto INDEXED_TAGGED_DATA = taggedDataStorage( + std::make_index_sequence()>{}); + +/// Get the tagged data for a specific enum value and tag at compile time. +/// +/// @tparam V Enum value +/// @tparam Tag Tag type +/// @returns The tagged data as a QStringView for the enum value. +template +[[nodiscard]] consteval QStringView enumTaggedData() noexcept +{ + return QStringView{ + detail::fromArray(detail::TAGGED_DATA)}; +} + +/// Get the tagged data for a runtime enum value and tag. +/// +/// @tparam Tag Tag type +/// @param value Enum value +/// @returns The tagged data as a QStringView for the enum value, or an empty view if value is invalid. +template +[[nodiscard]] constexpr QStringView enumTaggedData(E value) noexcept +{ + using D = std::decay_t; + + if (const auto i = magic_enum::enum_index(value)) + { + return detail::INDEXED_TAGGED_DATA[*i]; + } + return {}; +} + +} // namespace detail + +/// Get the display name of a specific enum value as a QStringView at compile time. +/// +/// If the enum type provides a qmagicenumDisplayName specialization, it is used. Otherwise, fall back to magic_enum's name. +/// +/// @tparam V Enum value +/// @returns Display name as QStringView, or name as fallback. +template +[[nodiscard]] consteval QStringView enumDisplayName() noexcept +{ + if constexpr (requires { qmagicenumDisplayName(V); }) + { + return detail::enumTaggedData(); + } + + // Fall back to our qmagicenum "enum name" implementation + return enumName(); +} + +/// Get the display name of a runtime enum value as a QStringView. +/// +/// If the enum type provides a qmagicenumDisplayName specialization, it is used. Otherwise, fall back to magic_enum's name. +/// +/// @param value Enum value +/// @returns Display name as QStringView, or name as fallback, or an empty string if not available. +template +[[nodiscard]] constexpr QStringView enumDisplayName(E value) noexcept +{ + if constexpr (requires { qmagicenumDisplayName(value); }) + { + using D = std::decay_t; + + return detail::enumTaggedData(value); + } + + // Fall back to our qmagicenum "enum name" implementation + return enumName(value); +} + +/// Get the display name of a specific enum value as a static QString. +/// +/// If the enum type provides a qmagicenumDisplayName specialization, it is used. Otherwise, fall back to magic_enum's name. +/// +/// @tparam V Enum value +/// @returns Display name as QString. The returned string is static. +template +[[nodiscard]] inline QString enumDisplayNameString() noexcept +{ + return detail::staticString(enumDisplayName()); +} + +/// Get the display name of a runtime enum value as a static QString. +/// +/// If the enum type provides a qmagicenumDisplayName specialization, it is used. Otherwise, fall back to magic_enum's name. +/// +/// @param value Enum value +/// @returns Display name as QString. Returns an empty string if value is invalid. The returned string is static. +template +[[nodiscard]] inline QString enumDisplayNameString(E value) noexcept +{ + using D = std::decay_t; + + return detail::staticString(enumDisplayName(value)); +} + +} // namespace chatterino::qmagicenum diff --git a/src/widgets/settingspages/SettingWidget.hpp b/src/widgets/settingspages/SettingWidget.hpp index e0e37a7b..ebfd5559 100644 --- a/src/widgets/settingspages/SettingWidget.hpp +++ b/src/widgets/settingspages/SettingWidget.hpp @@ -1,12 +1,15 @@ #pragma once #include "common/ChatterinoSetting.hpp" +#include "common/QLogging.hpp" #include "util/QMagicEnum.hpp" +#include "util/QMagicEnumTagged.hpp" #include "widgets/settingspages/GeneralPageView.hpp" #include #include #include +#include #include #include #include @@ -59,10 +62,14 @@ public: auto *lbl = new QLabel(label % ":"); auto *combo = new ComboBox; combo->setFocusPolicy(Qt::StrongFocus); - for (const auto &item : qmagicenum::enumNames()) + + for (const auto value : magic_enum::enum_values()) { - combo->addItem(item.toString()); + combo->addItem( + qmagicenum::enumDisplayNameString(value), + QVariant(static_cast>(value))); } + // TODO: this can probably use some other size hint/size strategy combo->setMinimumWidth(combo->minimumSizeHint().width()); @@ -87,11 +94,18 @@ public: QObject::connect( combo, &QComboBox::currentTextChanged, - [&setting](const auto &newText) { - // The setter for EnumStringSetting does not check that this value is valid - // Instead, it's up to the getters to make sure that the setting is legic - see the enum_cast above - // You could also use the settings `getEnum` function - setting = newText; + [label, combo, &setting](const auto &newText) { + bool ok = true; + auto enumValue = combo->currentData().toInt(&ok); + if (!ok) + { + qCWarning(chatterinoWidget) + << "Combo" << label << " with value" << newText + << "did not contain an intable UserRole data"; + return; + } + + setting = qmagicenum::enumNameString(static_cast(enumValue)); }); return widget; diff --git a/tests/src/QMagicEnum.cpp b/tests/src/QMagicEnum.cpp index 17acc986..2af93c17 100644 --- a/tests/src/QMagicEnum.cpp +++ b/tests/src/QMagicEnum.cpp @@ -3,11 +3,14 @@ #include "common/FlagsEnum.hpp" #include "common/Literals.hpp" #include "Test.hpp" +#include "util/QMagicEnumTagged.hpp" using namespace chatterino; using namespace literals; using qmagicenum::enumCast; +using qmagicenum::enumDisplayName; +using qmagicenum::enumDisplayNameString; using qmagicenum::enumFlagsName; using qmagicenum::enumName; using qmagicenum::enumNames; @@ -38,6 +41,19 @@ enum class MyCustom { Second = 9, }; +constexpr chatterino::qmagicenum::customize_t qmagicenumDisplayName( + MyCustom value) noexcept +{ + switch (value) + { + case MyCustom::First: + return "First (Display Name)"; + + default: + return {}; + } +} + enum MyOpen { OpenOne = 11, OpenTwo = 12, @@ -198,3 +214,47 @@ TEST(QMagicEnum, caseInsensitive) static_assert(checkInsensitive(MyCustom::Second, u"MySecond.*")); static_assert(checkInsensitive(OpenOne, u"openone")); } + +TEST(QMagicEnumTagged, displayName) +{ + // MyCustom has a display name specialization for First, but not for Default or Second + static_assert(eq(enumDisplayName(), u"Default")); + static_assert(eq(enumDisplayName(MyCustom::Default), u"Default")); + static_assert(eq(enumDisplayName(static_cast(1)), u"Default")); + static_assert( + eq(enumDisplayName(), u"First (Display Name)")); + static_assert( + eq(enumDisplayName(MyCustom::First), u"First (Display Name)")); + static_assert( + eq(enumDisplayName(static_cast(4)), u"First (Display Name)")); + static_assert(eq(enumDisplayName(), u"mysecond.*")); + static_assert(eq(enumDisplayName(MyCustom::Second), u"mysecond.*")); + static_assert(eq(enumDisplayName(static_cast(9)), u"mysecond.*")); + + // MyFlag does not have a display name specialization + static_assert(eq(enumDisplayName(), u"None")); + static_assert(eq(enumDisplayName(), u"One")); + static_assert(eq(enumDisplayName(), u"Two")); + static_assert(eq(enumDisplayName(), u"Four")); +} + +TEST(QMagicEnumTagged, enumDisplayNameString) +{ + auto withSpecDN = enumDisplayNameString(); + ASSERT_EQ(withSpecDN, u"First (Display Name)"); + + auto withSpec = enumName(); + ASSERT_EQ(withSpec, u"myfirst"); + + auto withoutSpecDN = enumDisplayNameString(); + ASSERT_EQ(withoutSpecDN, u"Eight"); + + auto withoutSpec = enumName(); + ASSERT_EQ(withoutSpec, u"Eight"); + + auto secondWithSpecDN = enumDisplayNameString(); + ASSERT_EQ(secondWithSpecDN, u"mysecond.*"); + + auto secondWithSpec = enumName(); + ASSERT_EQ(secondWithSpec, u"mysecond.*"); +}