From 92c921c2a143c0e124172b6c44917f7ea903dff8 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 30 Nov 2024 13:41:49 +0100 Subject: [PATCH] feat: add "tags" or "keyword" support for settings (#5585) this allows us to search for both "ffz" and "frankerfacez" when searching for FrankerFaceZ settings, without trying to force add both into the setting name --- CHANGELOG.md | 1 + resources/qss/settings.qss | 5 + src/CMakeLists.txt | 2 + src/widgets/settingspages/GeneralPage.cpp | 152 +++++++++++------- src/widgets/settingspages/GeneralPageView.cpp | 17 +- src/widgets/settingspages/GeneralPageView.hpp | 46 +----- src/widgets/settingspages/SettingWidget.cpp | 144 +++++++++++++++++ src/widgets/settingspages/SettingWidget.hpp | 107 ++++++++++++ 8 files changed, 364 insertions(+), 110 deletions(-) create mode 100644 src/widgets/settingspages/SettingWidget.cpp create mode 100644 src/widgets/settingspages/SettingWidget.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cbaff9a..3aacff6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ - Dev: Refactor and document `Scrollbar`. (#5334, #5393) - Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421, #5435) - Dev: Reduced the amount of scale events. (#5404, #5406) +- Dev: Refactored settings widget creation. (#5585) - Dev: Removed unused timegate settings. (#5361) - Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500) - Dev: Unsingletonize `Resources2`. (#5460) diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index 93c69b60..d6bcf3ee 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -61,6 +61,11 @@ chatterino--DescriptionLabel { color: #999; } +QLabel#description { + color: #999; + padding-left: 10px; +} + chatterino--NavigationLabel { font-family: "Segoe UI light"; font-size: 15px; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cfd593b8..e6385e2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -723,6 +723,8 @@ set(SOURCE_FILES widgets/settingspages/PluginsPage.hpp widgets/settingspages/SettingsPage.cpp widgets/settingspages/SettingsPage.hpp + widgets/settingspages/SettingWidget.cpp + widgets/settingspages/SettingWidget.hpp widgets/splits/ClosedSplits.cpp widgets/splits/ClosedSplits.hpp diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 7ddb8515..e85582fa 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -20,6 +20,7 @@ #include "util/IncognitoBrowser.hpp" #include "widgets/BaseWindow.hpp" #include "widgets/settingspages/GeneralPageView.hpp" +#include "widgets/settingspages/SettingWidget.hpp" #include #include @@ -265,10 +266,13 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, false, "Choose which tabs are visible in the notebook"); - layout.addCheckbox( - "Show message reply context", s.hideReplyContext, true, - "This setting will only affect how messages are shown. You can reply " - "to a message regardless of this setting."); + SettingWidget::inverseCheckbox("Show message reply context", + s.hideReplyContext) + ->setTooltip( + "This setting will only affect how messages are shown. You can " + "reply to a message regardless of this setting.") + ->addTo(layout); + layout.addCheckbox("Show message reply button", s.showReplyButton, false, "Show a reply button next to every chat message"); @@ -571,7 +575,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) s.removeSpacesBetweenEmotes, false, "When enabled, adjacent emotes will no longer have an " "added space seperating them."); - layout.addCheckbox("Show unlisted 7TV emotes", s.showUnlistedSevenTVEmotes); + SettingWidget::checkbox("Show unlisted 7TV emotes", + s.showUnlistedSevenTVEmotes) + ->addKeywords({"seventv"}) + ->addTo(layout); // TODO: Add a tooltip explaining what an unlisted 7TV emote is // but wait until https://github.com/Chatterino/wiki/pull/255 is resolved, // as an official description from 7TV devs is best @@ -622,25 +629,48 @@ void GeneralPage::initLayout(GeneralPageView &layout) "Google", }, s.emojiSet); - layout.addCheckbox("Show BetterTTV global emotes", - s.enableBTTVGlobalEmotes); - layout.addCheckbox("Show BetterTTV channel emotes", - s.enableBTTVChannelEmotes); - layout.addCheckbox("Enable BetterTTV live emote updates (requires restart)", - s.enableBTTVLiveUpdates); - layout.addCheckbox("Show FrankerFaceZ global emotes", - s.enableFFZGlobalEmotes); - layout.addCheckbox("Show FrankerFaceZ channel emotes", - s.enableFFZChannelEmotes); - layout.addCheckbox("Show 7TV global emotes", s.enableSevenTVGlobalEmotes); - layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes); - layout.addCheckbox("Enable 7TV live emote updates (requires restart)", - s.enableSevenTVEventAPI); - layout.addCheckbox("Send activity to 7TV", s.sendSevenTVActivity, false, - "When enabled, Chatterino will signal an activity to " - "7TV when you send a chat mesage. This is used for " - "badges, paints, and personal emotes. When disabled, no " - "activity is sent and others won't see your cosmetics."); + SettingWidget::checkbox("Show BetterTTV global emotes", + s.enableBTTVGlobalEmotes) + ->addKeywords({"bttv"}) + ->addTo(layout); + SettingWidget::checkbox("Show BetterTTV channel emotes", + s.enableBTTVChannelEmotes) + ->addKeywords({"bttv"}) + ->addTo(layout); + SettingWidget::checkbox( + "Enable BetterTTV live emote updates (requires restart)", + s.enableBTTVLiveUpdates) + ->addKeywords({"bttv"}) + ->addTo(layout); + + SettingWidget::checkbox("Show FrankerFaceZ global emotes", + s.enableFFZGlobalEmotes) + ->addKeywords({"ffz"}) + ->addTo(layout); + SettingWidget::checkbox("Show FrankerFaceZ channel emotes", + s.enableFFZChannelEmotes) + ->addKeywords({"ffz"}) + ->addTo(layout); + + SettingWidget::checkbox("Show 7TV global emotes", + s.enableSevenTVGlobalEmotes) + ->addKeywords({"seventv"}) + ->addTo(layout); + SettingWidget::checkbox("Show 7TV channel emotes", + s.enableSevenTVChannelEmotes) + ->addKeywords({"seventv"}) + ->addTo(layout); + SettingWidget::checkbox("Enable 7TV live emote updates (requires restart)", + s.enableSevenTVEventAPI) + ->addKeywords({"seventv"}) + ->addTo(layout); + SettingWidget::checkbox("Send activity to 7TV", s.sendSevenTVActivity) + ->setTooltip("When enabled, Chatterino will signal an activity to 7TV " + "when you send a chat mesage. This is used for badges, " + "paints, and personal emotes. When disabled, no activity " + "is sent and others won't see your cosmetics.") + ->addKeywords({"seventv"}) + ->addTo(layout); layout.addTitle("Streamer Mode"); layout.addDescription( @@ -976,15 +1006,23 @@ void GeneralPage::initLayout(GeneralPageView &layout) "e.g. prime, bits, sub gifter"); layout.addCheckbox("Chatterino", s.showBadgesChatterino, false, "e.g. Chatterino Supporter/Contributor/Developer"); - layout.addCheckbox("FrankerFaceZ", s.showBadgesFfz, false, - "e.g. Bot, FFZ supporter, FFZ developer"); - layout.addCheckbox("7TV", s.showBadgesSevenTV, false, - "Badges for 7TV admins, developers, and supporters"); + SettingWidget::checkbox("FrankerFaceZ", s.showBadgesFfz) + ->addKeywords({"ffz"}) + ->setTooltip("e.g. Bot, FrankerFaceZ supporter, FrankerFaceZ developer") + ->addTo(layout); + SettingWidget::checkbox("7TV", s.showBadgesSevenTV) + ->addKeywords({"seventv"}) + ->setTooltip("Badges for 7TV admins, developers, and supporters") + ->addTo(layout); layout.addSeperator(); - layout.addCheckbox("Use custom FrankerFaceZ moderator badges", - s.useCustomFfzModeratorBadges); - layout.addCheckbox("Use custom FrankerFaceZ VIP badges", - s.useCustomFfzVipBadges); + SettingWidget::checkbox("Use custom FrankerFaceZ moderator badges", + s.useCustomFfzModeratorBadges) + ->addKeywords({"ffz"}) + ->addTo(layout); + SettingWidget::checkbox("Use custom FrankerFaceZ VIP badges", + s.useCustomFfzVipBadges) + ->addKeywords({"ffz"}) + ->addTo(layout); layout.addSubtitle("Overlay"); layout.addIntInput( @@ -1080,10 +1118,11 @@ void GeneralPage::initLayout(GeneralPageView &layout) false, "Make all clickable links lowercase to deter " "phishing attempts."); - layout.addCheckbox( - "Show user's pronouns in user card", s.showPronouns, false, - "Shows users' pronouns in their user card. " - "Pronouns are retrieved from alejo.io when the user card is opened."); + SettingWidget::checkbox("Show user's pronouns in user card", s.showPronouns) + ->setDescription( + R"(Pronouns are retrieved from pr.alejo.io when a user card is opened.)") + ->addTo(layout); + layout.addCheckbox("Bold @usernames", s.boldUsernames, false, "Bold @mentions to make them more noticable."); layout.addCheckbox("Color @usernames", s.colorUsernames, false, @@ -1184,13 +1223,11 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addIntInput("Usercard scrollback limit (requires restart)", s.scrollbackUsercardLimit, 100, 100000, 100); - layout.addDropdownEnumClass( - "Show blocked term automod messages", - qmagicenum::enumNames(), - s.showBlockedTermAutomodMessages, - "Show messages that are blocked by AutoMod for containing a public " - "blocked term in the current channel.", - {}); + SettingWidget::dropdown("Show blocked term automod messages", + s.showBlockedTermAutomodMessages) + ->setTooltip("Show messages that are blocked by AutoMod for containing " + "a public blocked term in the current channel.") + ->addTo(layout); layout.addDropdown( "Stack timeouts", {"Stack", "Stack until timeout", "Don't stack"}, @@ -1216,25 +1253,20 @@ void GeneralPage::initLayout(GeneralPageView &layout) "@mention for the related thread. If the reply context is hidden, " "these mentions will never be stripped."); - layout.addDropdownEnumClass( - "Chat send protocol", qmagicenum::enumNames(), - s.chatSendProtocol, - "'Helix' will use Twitch's Helix API to send message. 'IRC' will use " - "IRC to send messages.", - {}); + SettingWidget::dropdown("Chat send protocol", s.chatSendProtocol) + ->setTooltip("'Helix' will use Twitch's Helix API to send message. " + "'IRC' will use IRC to send messages.") + ->addTo(layout); - layout.addCheckbox( - "Show send message button", s.showSendButton, false, - "Show a Send button next to each split input that can be " - "clicked to send the message"); + SettingWidget::checkbox("Show send message button", s.showSendButton) + ->setTooltip("Show a Send button next to each split input that can be " + "clicked to send the message") + ->addTo(layout); - auto *soundBackend = layout.addDropdownEnumClass( - "Sound backend (requires restart)", - qmagicenum::enumNames(), s.soundBackend, - "Change this only if you're noticing issues with sound playback on " - "your system", - {}); - soundBackend->setMinimumWidth(soundBackend->minimumSizeHint().width()); + SettingWidget::dropdown("Sound backend (requires restart)", s.soundBackend) + ->setTooltip("Change this only if you're noticing issues " + "with sound playback on your system") + ->addTo(layout); layout.addStretch(); diff --git a/src/widgets/settingspages/GeneralPageView.cpp b/src/widgets/settingspages/GeneralPageView.cpp index 29399258..754ff21f 100644 --- a/src/widgets/settingspages/GeneralPageView.cpp +++ b/src/widgets/settingspages/GeneralPageView.cpp @@ -6,6 +6,7 @@ #include "widgets/dialogs/ColorPickerDialog.hpp" #include "widgets/helper/color/ColorButton.hpp" #include "widgets/helper/Line.hpp" +#include "widgets/settingspages/SettingWidget.hpp" #include #include @@ -44,9 +45,16 @@ GeneralPageView::GeneralPageView(QWidget *parent) }); } -void GeneralPageView::addWidget(QWidget *widget) +void GeneralPageView::addWidget(QWidget *widget, QStringList keywords) { this->contentLayout_->addWidget(widget); + if (!keywords.isEmpty()) + { + this->groups_.back().widgets.push_back({ + .element = widget, + .keywords = keywords, + }); + } } void GeneralPageView::addLayout(QLayout *layout) @@ -376,11 +384,10 @@ bool GeneralPageView::filterElements(const QString &query) currentSubtitleVisible = true; widget.element->show(); groupAny = true; + break; } - else - { - widget.element->hide(); - } + + widget.element->hide(); } } diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index 4ce1c5b4..01a1a436 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -95,7 +95,7 @@ class GeneralPageView : public QWidget public: GeneralPageView(QWidget *parent = nullptr); - void addWidget(QWidget *widget); + void addWidget(QWidget *widget, QStringList keywords = {}); void addLayout(QLayout *layout); void addStretch(); @@ -274,50 +274,6 @@ public: return combo; } - template - ComboBox *addDropdownEnumClass(const QString &text, - const std::array &items, - EnumStringSetting &setting, - QString toolTipText, - const QString &defaultValueText) - { - auto *combo = this->addDropdown(text, {}, std::move(toolTipText)); - - for (const auto &item : items) - { - combo->addItem(item.toString()); - } - - if (!defaultValueText.isEmpty()) - { - combo->setCurrentText(defaultValueText); - } - - setting.connect( - [&setting, combo](const QString &value) { - auto enumValue = - qmagicenum::enumCast(value, qmagicenum::CASE_INSENSITIVE) - .value_or(setting.defaultValue); - - auto i = magic_enum::enum_integer(enumValue); - - combo->setCurrentIndex(i); - }, - this->managedConnections_); - - 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; - getApp()->getWindows()->forceLayoutChannelViews(); - }); - - return combo; - } - void enableIf(QComboBox *widget, auto &setting, auto cb) { auto updateVisibility = [cb = std::move(cb), &setting, widget]() { diff --git a/src/widgets/settingspages/SettingWidget.cpp b/src/widgets/settingspages/SettingWidget.cpp new file mode 100644 index 00000000..c4d5a8d5 --- /dev/null +++ b/src/widgets/settingspages/SettingWidget.cpp @@ -0,0 +1,144 @@ +#include "widgets/settingspages/SettingWidget.hpp" + +#include "widgets/settingspages/GeneralPageView.hpp" + +#include +#include +#include + +namespace { + +constexpr int MAX_TOOLTIP_LINE_LENGTH = 50; +const auto MAX_TOOLTIP_LINE_LENGTH_PATTERN = + QStringLiteral(R"(.{%1}\S*\K(\s+))").arg(MAX_TOOLTIP_LINE_LENGTH); +const QRegularExpression MAX_TOOLTIP_LINE_LENGTH_REGEX( + MAX_TOOLTIP_LINE_LENGTH_PATTERN); + +} // namespace + +namespace chatterino { + +SettingWidget::SettingWidget(const QString &mainKeyword) + : vLayout(new QVBoxLayout(this)) + , hLayout(new QHBoxLayout) +{ + this->vLayout->setContentsMargins(0, 0, 0, 0); + + this->hLayout->setContentsMargins(0, 0, 0, 0); + this->vLayout->addLayout(hLayout); + + this->keywords.append(mainKeyword); +} + +SettingWidget *SettingWidget::checkbox(const QString &label, + BoolSetting &setting) +{ + auto *widget = new SettingWidget(label); + + auto *check = new QCheckBox(label); + + widget->hLayout->addWidget(check); + + // update when setting changes + setting.connect( + [check](const bool &value, auto) { + check->setChecked(value); + }, + widget->managedConnections); + + // update setting on toggle + QObject::connect(check, &QCheckBox::toggled, widget, + [&setting](bool state) { + setting = state; + }); + + widget->actionWidget = check; + widget->label = check; + + return widget; +} + +SettingWidget *SettingWidget::inverseCheckbox(const QString &label, + BoolSetting &setting) +{ + auto *widget = new SettingWidget(label); + + auto *check = new QCheckBox(label); + + widget->hLayout->addWidget(check); + + // update when setting changes + setting.connect( + [check](const bool &value, auto) { + check->setChecked(!value); + }, + widget->managedConnections); + + // update setting on toggle + QObject::connect(check, &QCheckBox::toggled, widget, + [&setting](bool state) { + setting = !state; + }); + + widget->actionWidget = check; + widget->label = check; + + return widget; +} + +SettingWidget *SettingWidget::setTooltip(QString tooltip) +{ + assert(!tooltip.isEmpty()); + + if (tooltip.length() > MAX_TOOLTIP_LINE_LENGTH) + { + // match MAX_TOOLTIP_LINE_LENGTH characters, any remaining + // non-space, and then capture the following space for + // replacement with newline + tooltip.replace(MAX_TOOLTIP_LINE_LENGTH_REGEX, "\n"); + } + + if (this->label != nullptr) + { + this->label->setToolTip(tooltip); + } + + if (this->actionWidget != nullptr) + { + this->actionWidget->setToolTip(tooltip); + } + + this->keywords.append(tooltip); + + return this; +} + +SettingWidget *SettingWidget::setDescription(const QString &text) +{ + auto *lbl = new QLabel(text); + lbl->setTextInteractionFlags(Qt::TextBrowserInteraction | + Qt::LinksAccessibleByKeyboard); + lbl->setOpenExternalLinks(true); + lbl->setWordWrap(true); + lbl->setObjectName("description"); + + this->vLayout->insertWidget(0, lbl); + + this->keywords.append(text); + + return this; +} + +SettingWidget *SettingWidget::addKeywords(const QStringList &newKeywords) +{ + this->keywords.append(newKeywords); + + return this; +} + +void SettingWidget::addTo(GeneralPageView &view) +{ + view.addWidget(this, this->keywords); +} + +} // namespace chatterino diff --git a/src/widgets/settingspages/SettingWidget.hpp b/src/widgets/settingspages/SettingWidget.hpp new file mode 100644 index 00000000..b558b18a --- /dev/null +++ b/src/widgets/settingspages/SettingWidget.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include "common/ChatterinoSetting.hpp" +#include "util/QMagicEnum.hpp" +#include "widgets/settingspages/GeneralPageView.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chatterino { + +class GeneralPageView; + +class SettingWidget : QWidget +{ + Q_OBJECT + + explicit SettingWidget(const QString &mainKeyword); + +public: + ~SettingWidget() override = default; + SettingWidget &operator=(const SettingWidget &) = delete; + SettingWidget &operator=(SettingWidget &&) = delete; + SettingWidget(const SettingWidget &other) = delete; + SettingWidget(SettingWidget &&other) = delete; + + static SettingWidget *checkbox(const QString &label, BoolSetting &setting); + static SettingWidget *inverseCheckbox(const QString &label, + BoolSetting &setting); + template + static SettingWidget *dropdown(const QString &label, + EnumStringSetting &setting) + { + auto *widget = new SettingWidget(label); + + auto *lbl = new QLabel(label % ":"); + auto *combo = new ComboBox; + combo->setFocusPolicy(Qt::StrongFocus); + for (const auto &item : qmagicenum::enumNames()) + { + combo->addItem(item.toString()); + } + // TODO: this can probably use some other size hint/size strategy + combo->setMinimumWidth(combo->minimumSizeHint().width()); + + widget->actionWidget = combo; + widget->label = lbl; + + widget->hLayout->addWidget(lbl); + widget->hLayout->addStretch(1); + widget->hLayout->addWidget(combo); + + setting.connect( + [&setting, combo](const QString &value) { + auto enumValue = + qmagicenum::enumCast(value, qmagicenum::CASE_INSENSITIVE) + .value_or(setting.defaultValue); + + auto i = magic_enum::enum_integer(enumValue); + + combo->setCurrentIndex(i); + }, + widget->managedConnections); + + 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; + }); + + return widget; + } + + SettingWidget *setTooltip(QString tooltip); + SettingWidget *setDescription(const QString &text); + + /// Add extra keywords to the widget + /// + /// All text from the tooltip, description, and label are already keywords + SettingWidget *addKeywords(const QStringList &newKeywords); + + void addTo(GeneralPageView &view); + +private: + QWidget *label = nullptr; + QWidget *actionWidget = nullptr; + + QVBoxLayout *vLayout; + QHBoxLayout *hLayout; + + pajlada::Signals::SignalHolder managedConnections; + + QStringList keywords; +}; + +} // namespace chatterino