diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a08aac48..277cb267b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,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 93c69b603..d6bcf3ee3 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 9d7134b97..87fb65e31 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -713,6 +713,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 7103fb527..663d3b5ed 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"); @@ -614,10 +618,19 @@ void GeneralPage::initLayout(GeneralPageView &layout) "Google", }, s.emojiSet); - layout.addCheckbox("Show BTTV global emotes", s.enableBTTVGlobalEmotes); - layout.addCheckbox("Show BTTV channel emotes", s.enableBTTVChannelEmotes); - layout.addCheckbox("Enable BTTV live emote updates (requires restart)", - s.enableBTTVLiveUpdates); + 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); layout.addCheckbox("Show FFZ global emotes", s.enableFFZGlobalEmotes); layout.addCheckbox("Show FFZ channel emotes", s.enableFFZChannelEmotes); layout.addCheckbox("Show 7TV global emotes", s.enableSevenTVGlobalEmotes); @@ -1063,10 +1076,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, @@ -1191,25 +1205,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 293992581..754ff21f3 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 4ce1c5b4e..41e5efbed 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -21,6 +21,7 @@ class QScrollArea; namespace chatterino { class ColorButton; +class SettingWidget; class Space : public QLabel { @@ -95,7 +96,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 +275,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 000000000..c4d5a8d57 --- /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 000000000..9d16a610a --- /dev/null +++ b/src/widgets/settingspages/SettingWidget.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include "common/ChatterinoSetting.hpp" +#include "util/QMagicEnum.hpp" +#include "widgets/settingspages/GeneralPageView.hpp" + +#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