From 34429671ba8d17add4496131982ceba93644073d Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 3 Mar 2025 16:33:13 +0100 Subject: [PATCH] refactor: use some more SettingWidget & restyle external tools page (#6023) --- CHANGELOG.md | 1 + src/singletons/Settings.hpp | 38 +++- src/util/StreamLink.cpp | 13 +- .../settingspages/ExternalToolsPage.cpp | 195 ++++++++--------- .../settingspages/ExternalToolsPage.hpp | 8 + src/widgets/settingspages/GeneralPage.cpp | 120 +++++++---- src/widgets/settingspages/GeneralPageView.cpp | 201 ++++++++++-------- src/widgets/settingspages/GeneralPageView.hpp | 37 ++-- src/widgets/settingspages/SettingWidget.cpp | 174 ++++++++++++++- src/widgets/settingspages/SettingWidget.hpp | 23 ++ 10 files changed, 545 insertions(+), 265 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d750ea..34066c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Dev: Fixed duplicate CMake configure in clean builds. (#5940) - Dev: BTTV emotes are now loaded as WEBP. (#5957) - Dev: Reduced time we wait for PubSub connections to cleanly exit from 1s to 100ms. (#6019) +- Dev: Refactored some settings styles/APIs. (#6023) - Dev: Added snapshot tests for EventSub. (#5965) - Dev: Removed dead code and some MSVC warnings. (#6024) diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 3c26f46e..8fff5b4f 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -76,6 +76,15 @@ enum class ShowModerationState : int { Never = 1, }; +enum class StreamLinkPreferredQuality : std::uint8_t { + Choose, + Source, + High, + Medium, + Low, + AudioOnly, +}; + enum StreamerModeSetting { Disabled = 0, Enabled = 1, @@ -538,8 +547,10 @@ public: BoolSetting streamlinkUseCustomPath = {"/external/streamlink/useCustomPath", false}; QStringSetting streamlinkPath = {"/external/streamlink/customPath", ""}; - QStringSetting preferredQuality = {"/external/streamlink/quality", - "Choose"}; + EnumStringSetting preferredQuality = { + "/external/streamlink/quality", + StreamLinkPreferredQuality::Choose, + }; QStringSetting streamlinkOpts = {"/external/streamlink/options", ""}; // Custom URI Scheme @@ -700,3 +711,26 @@ private: Settings *getSettings(); } // namespace chatterino + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::StreamLinkPreferredQuality value) noexcept +{ + using chatterino::StreamLinkPreferredQuality; + switch (value) + { + case chatterino::StreamLinkPreferredQuality::Choose: + case chatterino::StreamLinkPreferredQuality::Source: + case chatterino::StreamLinkPreferredQuality::High: + case chatterino::StreamLinkPreferredQuality::Medium: + case chatterino::StreamLinkPreferredQuality::Low: + return default_tag; + + case chatterino::StreamLinkPreferredQuality::AudioOnly: + return "Audio only"; + + default: + return default_tag; + } +} diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index 6bb6b6fc..33e7b5a4 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -199,10 +199,9 @@ void openStreamlinkForChannel(const QString &channel) QString channelURL = "twitch.tv/" + channel; - QString preferredQuality = getSettings()->preferredQuality.getValue(); - preferredQuality = preferredQuality.toLower(); + auto preferredQuality = getSettings()->preferredQuality.getEnum(); - if (preferredQuality == "choose") + if (preferredQuality == StreamLinkPreferredQuality::Choose) { getStreamQualities(channelURL, [=](QStringList qualityOptions) { QualityPopup::showDialog(channelURL, qualityOptions); @@ -218,22 +217,22 @@ void openStreamlinkForChannel(const QString &channel) // Streamlink qualities to exclude QString exclude; - if (preferredQuality == "high") + if (preferredQuality == StreamLinkPreferredQuality::High) { exclude = ">720p30"; quality = "high,best"; } - else if (preferredQuality == "medium") + else if (preferredQuality == StreamLinkPreferredQuality::Medium) { exclude = ">540p30"; quality = "medium,best"; } - else if (preferredQuality == "low") + else if (preferredQuality == StreamLinkPreferredQuality::Low) { exclude = ">360p30"; quality = "low,best"; } - else if (preferredQuality == "audio only") + else if (preferredQuality == StreamLinkPreferredQuality::AudioOnly) { quality = "audio,audio_only"; } diff --git a/src/widgets/settingspages/ExternalToolsPage.cpp b/src/widgets/settingspages/ExternalToolsPage.cpp index 18e3a2e0..3bac77d6 100644 --- a/src/widgets/settingspages/ExternalToolsPage.cpp +++ b/src/widgets/settingspages/ExternalToolsPage.cpp @@ -2,9 +2,8 @@ #include "singletons/Settings.hpp" #include "util/Helpers.hpp" -#include "util/LayoutCreator.hpp" -#include "util/RemoveScrollAreaBackground.hpp" #include "util/StreamLink.hpp" +#include "widgets/settingspages/SettingWidget.hpp" #include #include @@ -13,31 +12,47 @@ namespace chatterino { inline const QStringList STREAMLINK_QUALITY = { - "Choose", "Source", "High", "Medium", "Low", "Audio only"}; + "Choose", "Source", "High", "Medium", "Low", "Audio only", +}; ExternalToolsPage::ExternalToolsPage() + : view(GeneralPageView::withoutNavigation(this)) { - LayoutCreator layoutCreator(this); + auto *y = new QVBoxLayout; + auto *x = new QHBoxLayout; + x->addWidget(this->view); + auto *z = new QFrame; + z->setLayout(x); + y->addWidget(z); + this->setLayout(y); - auto scroll = layoutCreator.emplace(); - auto widget = scroll.emplaceScrollAreaWidget(); - removeScrollAreaBackground(scroll.getElement(), widget.getElement()); + this->initLayout(*view); +} - auto layout = widget.setLayoutType(); +bool ExternalToolsPage::filterElements(const QString &query) +{ + if (this->view) + { + return this->view->filterElements(query) || query.isEmpty(); + } + + return false; +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void ExternalToolsPage::initLayout(GeneralPageView &layout) +{ + auto &s = *getSettings(); { - auto group = layout.emplace("Streamlink"); - auto groupLayout = group.setLayoutType(); - - auto *description = new QLabel( - "Streamlink is a command-line utility that pipes video streams " - "from various " - "services into a video player, such as VLC. Make sure to edit the " - "configuration file before you use it!"); - description->setWordWrap(true); - description->setStyleSheet("color: #bbb"); - - auto *links = new QLabel( + auto *form = new QFormLayout; + layout.addTitle("Streamlink"); + layout.addDescription("Streamlink is a command-line utility that pipes " + "video streams from " + "various services into a video player, such as " + "VLC. Make sure to edit " + "the configuration file before you use it!"); + layout.addDescription( formatRichNamedLink("https://streamlink.github.io/", "Website") + " " + formatRichNamedLink( @@ -46,115 +61,83 @@ ExternalToolsPage::ExternalToolsPage() " " + formatRichNamedLink("https://streamlink.github.io/cli.html#twitch", "Documentation")); - links->setTextFormat(Qt::RichText); - links->setTextInteractionFlags(Qt::TextBrowserInteraction | - Qt::LinksAccessibleByKeyboard | - Qt::LinksAccessibleByMouse); - links->setOpenExternalLinks(true); - groupLayout->setWidget(0, QFormLayout::SpanningRole, description); - groupLayout->setWidget(1, QFormLayout::SpanningRole, links); + SettingWidget::checkbox("Use custom path (Enable if using non-standard " + "streamlink installation path)", + s.streamlinkUseCustomPath) + ->addTo(layout); - auto *customPathCb = - this->createCheckBox("Use custom path (Enable if using " - "non-standard streamlink installation path)", - getSettings()->streamlinkUseCustomPath); - groupLayout->setWidget(2, QFormLayout::SpanningRole, customPathCb); - - auto *note = new QLabel( + layout.addDescription( QStringLiteral( "Chatterino expects the executable to be called \"%1\".") .arg(STREAMLINK_BINARY_NAME)); - note->setWordWrap(true); - note->setStyleSheet("color: #bbb"); - groupLayout->setWidget(3, QFormLayout::SpanningRole, note); - auto *customPath = this->createLineEdit(getSettings()->streamlinkPath); - customPath->setPlaceholderText( - "Path to folder where Streamlink executable can be found"); - groupLayout->addRow("Custom streamlink path:", customPath); - groupLayout->addRow( - "Preferred quality:", - this->createComboBox(STREAMLINK_QUALITY, - getSettings()->preferredQuality)); - groupLayout->addRow( - "Additional options:", - this->createLineEdit(getSettings()->streamlinkOpts)); + layout.addLayout(form); - getSettings()->streamlinkUseCustomPath.connect( - [=](const auto &value, auto) { - customPath->setEnabled(value); - }, - this->managedConnections_); + SettingWidget::lineEdit( + "Custom streamlink path", s.streamlinkPath, + "Path to folder where Streamlink executable can be found") + ->conditionallyEnabledBy(s.streamlinkUseCustomPath) + ->addTo(layout, form); + + SettingWidget::dropdown("Preferred quality", s.preferredQuality) + ->addTo(layout, form); + + SettingWidget::lineEdit("Additional options", s.streamlinkOpts, "") + ->addTo(layout, form); } - layout->addSpacing(16); { - auto group = layout.emplace("Custom stream player"); - auto groupLayout = group.setLayoutType(); + layout.addTitle("Custom stream player"); + layout.addDescription( + "You can open Twitch streams directly in any video player that has " + "built-in Twitch support and has own URI Scheme.\nE.g.: IINA for " + "macOS and Potplayer (with extension) for Windows.\n\nWith this " + "value set, you will get the option to \"Open in custom player\" " + "when right-clicking a channel header."); - auto *description = new QLabel( - "You can open Twitch streams directly in any video player that " - "has built-in Twitch support and has own URI Scheme.\nE.g.: " - "IINA for macOS and Potplayer (with extension) for " - "Windows.\n\nWith this value set, you will get the option to " - "\"Open in custom player\" when " - "right-clicking a channel header."); - description->setWordWrap(true); - description->setStyleSheet("color: #bbb"); - - groupLayout->setWidget(0, QFormLayout::SpanningRole, description); - - auto *lineEdit = this->createLineEdit(getSettings()->customURIScheme); - lineEdit->setPlaceholderText("custom-player-scheme://"); - groupLayout->addRow("Custom stream player URI Scheme:", lineEdit); + SettingWidget::lineEdit("Custom stream player URI Scheme", + s.customURIScheme, "custom-player-scheme://") + ->addTo(layout); } - layout->addSpacing(16); { - auto group = layout.emplace("Image Uploader"); - auto groupLayout = group.setLayoutType(); + auto *form = new QFormLayout; + layout.addTitle("Image Uploader"); - auto *description = new QLabel( - "You can set custom host for uploading images, like " - "imgur.com or s-ul.eu.
Check " + + layout.addDescription( + "You can set custom host for uploading images, like imgur.com or " + "s-ul.eu.
Check " + formatRichNamedLink("https://chatterino.com/help/image-uploader", "this guide") + " for help."); - description->setWordWrap(true); - description->setStyleSheet("color: #bbb"); - description->setTextFormat(Qt::RichText); - description->setTextInteractionFlags(Qt::TextBrowserInteraction | - Qt::LinksAccessibleByKeyboard | - Qt::LinksAccessibleByMouse); - description->setOpenExternalLinks(true); - groupLayout->setWidget(0, QFormLayout::SpanningRole, description); + SettingWidget::checkbox("Enable image uploader", s.imageUploaderEnabled) + ->addTo(layout); - groupLayout->addRow(this->createCheckBox( - "Enable image uploader", getSettings()->imageUploaderEnabled)); - groupLayout->addRow( - this->createCheckBox("Ask for confirmation when uploading an image", - getSettings()->askOnImageUpload)); + SettingWidget::checkbox("Ask for confirmation when uploading an image", + s.askOnImageUpload) + ->addTo(layout); - groupLayout->addRow( - "Request URL: ", - this->createLineEdit(getSettings()->imageUploaderUrl)); - groupLayout->addRow( - "Form field: ", - this->createLineEdit(getSettings()->imageUploaderFormField)); - groupLayout->addRow( - "Extra Headers: ", - this->createLineEdit(getSettings()->imageUploaderHeaders)); - groupLayout->addRow( - "Image link: ", - this->createLineEdit(getSettings()->imageUploaderLink)); - groupLayout->addRow( - "Deletion link: ", - this->createLineEdit(getSettings()->imageUploaderDeletionLink)); + layout.addLayout(form); + + SettingWidget::lineEdit("Request URL", s.imageUploaderUrl) + ->addTo(layout, form); + + SettingWidget::lineEdit("Form field", s.imageUploaderFormField) + ->addTo(layout, form); + + SettingWidget::lineEdit("Extra Headers", s.imageUploaderHeaders) + ->addTo(layout, form); + + SettingWidget::lineEdit("Image link", s.imageUploaderLink) + ->addTo(layout, form); + + SettingWidget::lineEdit("Deletion link", s.imageUploaderDeletionLink) + ->addTo(layout, form); } - layout->addStretch(1); + layout.addStretch(); } } // namespace chatterino diff --git a/src/widgets/settingspages/ExternalToolsPage.hpp b/src/widgets/settingspages/ExternalToolsPage.hpp index 5e2b9de7..316ae07d 100644 --- a/src/widgets/settingspages/ExternalToolsPage.hpp +++ b/src/widgets/settingspages/ExternalToolsPage.hpp @@ -1,5 +1,6 @@ #pragma once +#include "widgets/settingspages/GeneralPageView.hpp" #include "widgets/settingspages/SettingsPage.hpp" namespace chatterino { @@ -8,6 +9,13 @@ class ExternalToolsPage : public SettingsPage { public: ExternalToolsPage(); + + bool filterElements(const QString &query) override; + +private: + void initLayout(GeneralPageView &layout); + + GeneralPageView *view; }; } // namespace chatterino diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index a056119a..ea2b346c 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -95,7 +95,7 @@ GeneralPage::GeneralPage() { auto *y = new QVBoxLayout; auto *x = new QHBoxLayout; - auto *view = new GeneralPageView; + auto *view = GeneralPageView::withNavigation(this); this->view_ = view; x->addWidget(view); auto *z = new QFrame; @@ -275,8 +275,9 @@ void GeneralPage::initLayout(GeneralPageView &layout) "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"); + SettingWidget::checkbox("Show message reply button", s.showReplyButton) + ->setTooltip("Show a reply button next to every chat message") + ->addTo(layout); auto removeTabSeq = getApp()->getHotkeys()->getDisplaySequence( HotkeyCategory::Window, "removeTab"); @@ -309,9 +310,13 @@ void GeneralPage::initLayout(GeneralPageView &layout) .arg(settingsSeq.toString( QKeySequence::SequenceFormat::NativeText)); } - layout.addCheckbox("Show preferences button" + shortcut, - s.hidePreferencesButton, true); - layout.addCheckbox("Show user button", s.hideUserButton, true); + + SettingWidget::inverseCheckbox("Show preferences button" + shortcut, + s.hidePreferencesButton) + ->addTo(layout); + + SettingWidget::inverseCheckbox("Show user button", s.hideUserButton) + ->addTo(layout); } layout.addCheckbox("Mark tabs with live channels", s.showTabLive, false, "Shows a red dot in the top right corner of a tab to " @@ -464,24 +469,34 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, false); - layout.addCheckbox( - "Hide scrollbar thumb", s.hideScrollbarThumb, false, - "Hiding the scrollbar thumb (the handle you can drag) will disable " - "all mouse interaction in the scrollbar."); + SettingWidget::checkbox("Hide scrollbar thumb", s.hideScrollbarThumb) + ->setTooltip("Hiding the scrollbar thumb (the handle you can drag) " + "will disable all mouse interaction in the scrollbar.") + ->addKeywords({"scroll bar"}) + ->addTo(layout); - layout.addCheckbox("Hide scrollbar highlights", s.hideScrollbarHighlights, - false); + SettingWidget::checkbox("Hide scrollbar highlights", + s.hideScrollbarHighlights) + ->addKeywords({"scroll bar"}) + ->addTo(layout); layout.addTitle("Messages"); - layout.addCheckbox( - "Separate with lines", s.separateMessages, false, - "Adds a line between each message to help better tell them apart."); - layout.addCheckbox("Alternate background color", s.alternateMessages, false, - "Slightly change the background behind every other " - "message to help better tell them apart."); - layout.addCheckbox("Hide deleted messages", s.hideModerated, false, - "When enabled, messages deleted by moderators will " - "be hidden."); + + SettingWidget::checkbox("Separate with lines", s.separateMessages) + ->setTooltip( + "Adds a line between each message to help better tell them apart.") + ->addTo(layout); + + SettingWidget::checkbox("Alternate background color", s.alternateMessages) + ->setTooltip("Slightly change the background behind every other " + "message to help better tell them apart.") + ->addTo(layout); + + SettingWidget::checkbox("Hide deleted messages", s.hideModerated) + ->setTooltip( + "When enabled, messages deleted by moderators will be hidden.") + ->addTo(layout); + layout.addDropdown( "Timestamp format", {"Disable", "h:mm", "hh:mm", "h:mm a", "hh:mm a", "h:mm:ss", "hh:mm:ss", @@ -539,9 +554,8 @@ void GeneralPage::initLayout(GeneralPageView &layout) } }, false); - layout.addColorButton("Line color", - QColor(getSettings()->lastMessageColor.getValue()), - getSettings()->lastMessageColor); + + SettingWidget::colorButton("Line color", s.lastMessageColor)->addTo(layout); layout.addTitle("Emotes"); layout.addCheckbox("Enable", s.enableEmoteImages); @@ -1047,11 +1061,20 @@ void GeneralPage::initLayout(GeneralPageView &layout) true, "The final scale of the messages in the overlay is computed by " "multiplying this zoom factor with the global zoom level."); - layout.addIntInput( - "Background opacity (0-255)", s.overlayBackgroundOpacity, 0, 255, 1, - "Controls the opacity of the (possibly alternating) background behind " - "messages. The color is set through the current theme. 255 corresponds " - "to a fully opaque background."); + + SettingWidget::intInput("Background opacity (0-255)", + s.overlayBackgroundOpacity, + { + .min = 0, + .max = 255, + .singleStep = 1, + }) + ->setTooltip( + "Controls the opacity of the (possibly alternating) background " + "behind messages. The color is set through the current theme. 255 " + "corresponds to a fully opaque background.") + ->addTo(layout); + layout.addCheckbox("Enable Shadow", s.enableOverlayShadow, false, "Enables a drop shadow on the overlay. This will use " "more processing power."); @@ -1059,9 +1082,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) 1, "Controls the opacity of the added drop shadow. 255 " "corresponds to a fully opaque shadow."); - layout.addColorButton("Shadow color", - QColor(getSettings()->overlayShadowColor.getValue()), - getSettings()->overlayShadowColor); + + SettingWidget::colorButton("Shadow color", s.overlayShadowColor) + ->addTo(layout); + layout .addIntInput("Shadow radius", s.overlayShadowRadius, 0, 40, 1, "Controls how far the shadow is spread (the blur " @@ -1088,15 +1112,14 @@ void GeneralPage::initLayout(GeneralPageView &layout) s.openLinksIncognito); } - layout.addCustomCheckbox( + SettingWidget::customCheckbox( "Restart on crash (requires restart)", - [] { - return getApp()->getCrashHandler()->shouldRecover(); - }, + getApp()->getCrashHandler()->shouldRecover(), [](bool on) { - return getApp()->getCrashHandler()->saveShouldRecover(on); - }, - "When possible, restart Chatterino if the program crashes"); + getApp()->getCrashHandler()->saveShouldRecover(on); + }) + ->setTooltip("When possible, restart Chatterino if the program crashes") + ->addTo(layout); #if defined(Q_OS_LINUX) && !defined(NO_QTKEYCHAIN) if (!getApp()->getPaths().isPortable()) @@ -1107,13 +1130,18 @@ void GeneralPage::initLayout(GeneralPageView &layout) } #endif - layout.addCheckbox( - "Show moderation messages", s.hideModerationActions, true, - "Show messages for timeouts, bans, and other moderator actions."); - layout.addCheckbox("Show deletions of single messages", - s.hideDeletionActions, true, - "Show when a single message is deleted.\ne.g. A message " - "from TreuKS was deleted: abc"); + SettingWidget::inverseCheckbox("Show moderation messages", + s.hideModerationActions) + ->setTooltip( + "Show messages for timeouts, bans, and other moderator actions.") + ->addTo(layout); + + SettingWidget::inverseCheckbox("Show deletions of single messages", + s.hideDeletionActions) + ->setTooltip("Show when a single message is deleted.\ne.g. A message " + "from TreuKS was deleted: abc") + ->addTo(layout); + layout.addCheckbox( "Colorize users without color set (gray names)", s.colorizeNicknames, false, diff --git a/src/widgets/settingspages/GeneralPageView.cpp b/src/widgets/settingspages/GeneralPageView.cpp index 754ff21f..b3d4118f 100644 --- a/src/widgets/settingspages/GeneralPageView.cpp +++ b/src/widgets/settingspages/GeneralPageView.cpp @@ -1,10 +1,9 @@ #include "widgets/settingspages/GeneralPageView.hpp" #include "Application.hpp" +#include "common/QLogging.hpp" #include "util/LayoutHelper.hpp" #include "util/RapidJsonSerializeQString.hpp" -#include "widgets/dialogs/ColorPickerDialog.hpp" -#include "widgets/helper/color/ColorButton.hpp" #include "widgets/helper/Line.hpp" #include "widgets/settingspages/SettingWidget.hpp" @@ -25,30 +24,55 @@ namespace chatterino { GeneralPageView::GeneralPageView(QWidget *parent) : QWidget(parent) + , contentScrollArea_(new QScrollArea) + , contentLayout_(new QVBoxLayout) { - auto *scrollArea = this->contentScrollArea_ = - makeScrollArea(this->contentLayout_ = new QVBoxLayout); - scrollArea->setObjectName("generalSettingsScrollContent"); - - auto *navigation = - wrapLayout(this->navigationLayout_ = makeLayout({})); - navigation->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); - this->navigationLayout_->setAlignment(Qt::AlignTop); - this->navigationLayout_->addSpacing(6); - - this->setLayout(makeLayout( - {scrollArea, new QSpacerItem(16, 1), navigation})); - - QObject::connect(scrollArea->verticalScrollBar(), &QScrollBar::valueChanged, - this, [this] { - this->updateNavigationHighlighting(); - }); + auto *contentWidget = new QWidget; + contentWidget->setLayout(this->contentLayout_); + this->contentScrollArea_->setWidget(contentWidget); + this->contentScrollArea_->setObjectName("generalSettingsScrollContent"); + this->contentScrollArea_->setWidgetResizable(true); } -void GeneralPageView::addWidget(QWidget *widget, QStringList keywords) +GeneralPageView *GeneralPageView::withoutNavigation(QWidget *parent) +{ + auto *view = new GeneralPageView(parent); + + view->setLayout(makeLayout({ + view->contentScrollArea_, + })); + + return view; +} + +GeneralPageView *GeneralPageView::withNavigation(QWidget *parent) +{ + auto *view = new GeneralPageView(parent); + + auto *navigation = + wrapLayout(view->navigationLayout_ = makeLayout({})); + navigation->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); + view->navigationLayout_->setAlignment(Qt::AlignTop); + view->navigationLayout_->addSpacing(6); + + view->setLayout(makeLayout({ + view->contentScrollArea_, + new QSpacerItem(16, 1), + navigation, + })); + + QObject::connect(view->contentScrollArea_->verticalScrollBar(), + &QScrollBar::valueChanged, view, [view] { + view->updateNavigationHighlighting(); + }); + + return view; +} + +void GeneralPageView::addWidget(QWidget *widget, const QStringList &keywords) { this->contentLayout_->addWidget(widget); - if (!keywords.isEmpty()) + if (!this->groups_.empty()) { this->groups_.back().widgets.push_back({ .element = widget, @@ -57,6 +81,25 @@ void GeneralPageView::addWidget(QWidget *widget, QStringList keywords) } } +void GeneralPageView::registerWidget(QWidget *widget, + const QStringList &keywords, + QWidget *parentElement) +{ + if (!this->groups_.empty()) + { + this->groups_.back().widgets.push_back({ + .element = widget, + .keywords = keywords, + .parentElement = parentElement, + }); + } +} + +void GeneralPageView::pushWidget(QWidget *widget) +{ + this->contentLayout_->addWidget(widget); +} + void GeneralPageView::addLayout(QLayout *layout) { this->contentLayout_->addLayout(layout); @@ -79,14 +122,21 @@ TitleLabel *GeneralPageView::addTitle(const QString &title) auto *label = new TitleLabel(title + ":"); this->addWidget(label); - // navigation item - auto *navLabel = new NavigationLabel(title); - navLabel->setCursor(Qt::PointingHandCursor); - this->navigationLayout_->addWidget(navLabel); + NavigationLabel *navLabel = nullptr; - QObject::connect(navLabel, &NavigationLabel::leftMouseUp, label, [=, this] { - this->contentScrollArea_->verticalScrollBar()->setValue(label->y()); - }); + // navigation item + if (this->navigationLayout_ != nullptr) + { + navLabel = new NavigationLabel(title); + navLabel->setCursor(Qt::PointingHandCursor); + this->navigationLayout_->addWidget(navLabel); + + QObject::connect( + navLabel, &NavigationLabel::leftMouseUp, label, [this, label] { + this->contentScrollArea_->verticalScrollBar()->setValue( + label->y()); + }); + } // groups this->groups_.push_back(Group{title, label, navLabel, nullptr, {}}); @@ -113,6 +163,12 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text, BoolSetting &setting, bool inverse, QString toolTipText) { + if (inverse) + { + qCWarning(chatterinoWidget) + << "use SettingWidget::inverseCheckbox instead"; + } + auto *check = new QCheckBox(text); this->addToolTip(*check, toolTipText); @@ -137,28 +193,6 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text, return check; } -QCheckBox *GeneralPageView::addCustomCheckbox(const QString &text, - const std::function &load, - std::function save, - const QString &toolTipText) -{ - auto *check = new QCheckBox(text); - this->addToolTip(*check, toolTipText); - - check->setChecked(load()); - - QObject::connect(check, &QCheckBox::toggled, this, - [save = std::move(save)](bool state) { - save(state); - }); - - this->addWidget(check); - - this->groups_.back().widgets.push_back({check, {text}}); - - return check; -} - ComboBox *GeneralPageView::addDropdown(const QString &text, const QStringList &list, QString toolTipText) @@ -211,43 +245,6 @@ ComboBox *GeneralPageView::addDropdown( return combo; } -ColorButton *GeneralPageView::addColorButton( - const QString &text, const QColor &color, - pajlada::Settings::Setting &setting, QString toolTipText) -{ - auto *colorButton = new ColorButton(color); - auto *layout = new QHBoxLayout(); - auto *label = new QLabel(text + ":"); - - layout->addWidget(label); - layout->addStretch(1); - layout->addWidget(colorButton); - - this->addToolTip(*label, toolTipText); - this->addLayout(layout); - - QObject::connect( - colorButton, &ColorButton::clicked, [this, &setting, colorButton]() { - auto *dialog = new ColorPickerDialog(QColor(setting), this); - // colorButton & setting are never deleted and the signal is deleted - // once the dialog is closed - QObject::connect(dialog, &ColorPickerDialog::colorConfirmed, this, - [&setting, colorButton](auto selected) { - if (selected.isValid()) - { - setting = selected.name(QColor::HexArgb); - colorButton->setColor(selected); - } - }); - dialog->show(); - }); - - this->groups_.back().widgets.push_back({label, {text}}); - this->groups_.back().widgets.push_back({colorButton, {text}}); - - return colorButton; -} - QSpinBox *GeneralPageView::addIntInput(const QString &text, IntSetting &setting, int min, int max, int step, QString toolTipText) @@ -289,6 +286,9 @@ QSpinBox *GeneralPageView::addIntInput(const QString &text, IntSetting &setting, void GeneralPageView::addNavigationSpacing() { + assert(this->navigationLayout_ != nullptr && + "addNavigationSpacing used without navigation"); + this->navigationLayout_->addSpacing(24); } @@ -341,10 +341,17 @@ bool GeneralPageView::filterElements(const QString &query) for (auto &&widget : group.widgets) { widget.element->show(); + if (widget.parentElement != nullptr) + { + widget.parentElement->show(); + } } group.title->show(); - group.navigationLink->show(); + if (group.navigationLink != nullptr) + { + group.navigationLink->show(); + } any = true; } // check if any match @@ -383,11 +390,19 @@ bool GeneralPageView::filterElements(const QString &query) { currentSubtitleVisible = true; widget.element->show(); + if (widget.parentElement != nullptr) + { + widget.parentElement->show(); + } groupAny = true; break; } widget.element->hide(); + if (widget.parentElement != nullptr) + { + widget.parentElement->hide(); + } } } @@ -402,7 +417,10 @@ bool GeneralPageView::filterElements(const QString &query) } group.title->setVisible(groupAny); - group.navigationLink->setVisible(groupAny); + if (group.navigationLink != nullptr) + { + group.navigationLink->setVisible(groupAny); + } any |= groupAny; } } @@ -412,6 +430,11 @@ bool GeneralPageView::filterElements(const QString &query) void GeneralPageView::updateNavigationHighlighting() { + if (this->navigationLayout_ == nullptr) + { + return; + } + auto scrollY = this->contentScrollArea_->verticalScrollBar()->value(); auto first = true; diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index 01a1a436..a9cf94b1 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -91,11 +91,22 @@ struct DropdownArgs { class GeneralPageView : public QWidget { Q_OBJECT - -public: GeneralPageView(QWidget *parent = nullptr); - void addWidget(QWidget *widget, QStringList keywords = {}); +public: + static GeneralPageView *withNavigation(QWidget *parent); + static GeneralPageView *withoutNavigation(QWidget *parent); + + void addWidget(QWidget *widget, const QStringList &keywords = {}); + + /// Register the widget with the given keywords. + /// This assumes that the widget is being held by a layout that has been added previously + void registerWidget(QWidget *widget, const QStringList &keywords, + QWidget *parentElement); + + /// Pushes the widget into the current layout + void pushWidget(QWidget *widget); + void addLayout(QLayout *layout); void addStretch(); @@ -104,19 +115,12 @@ public: /// @param inverse Inverses true to false and vice versa QCheckBox *addCheckbox(const QString &text, BoolSetting &setting, bool inverse = false, QString toolTipText = {}); - QCheckBox *addCustomCheckbox(const QString &text, - const std::function &load, - std::function save, - const QString &toolTipText = {}); ComboBox *addDropdown(const QString &text, const QStringList &items, QString toolTipText = {}); ComboBox *addDropdown(const QString &text, const QStringList &items, pajlada::Settings::Setting &setting, bool editable = false, QString toolTipText = {}); - ColorButton *addColorButton(const QString &text, const QColor &color, - pajlada::Settings::Setting &setting, - QString toolTipText = {}); QSpinBox *addIntInput(const QString &text, IntSetting &setting, int min, int max, int step, QString toolTipText = {}); void addNavigationSpacing(); @@ -299,8 +303,13 @@ private: void addToolTip(QWidget &widget, QString text) const; struct Widget { - QWidget *element; + /// The element of the register widget + /// This can point to the label of the widget, or the action widget (e.g. the spinbox) + QWidget *element{}; QStringList keywords; + + /// The optional parent element of the widget (usually pointing at a SettingWidget) + QWidget *parentElement{}; }; struct Group { @@ -311,9 +320,9 @@ private: std::vector widgets; }; - QScrollArea *contentScrollArea_; - QVBoxLayout *contentLayout_; - QVBoxLayout *navigationLayout_; + QScrollArea *contentScrollArea_ = nullptr; + QVBoxLayout *contentLayout_ = nullptr; + QVBoxLayout *navigationLayout_ = nullptr; std::vector groups_; pajlada::Signals::SignalHolder managedConnections_; diff --git a/src/widgets/settingspages/SettingWidget.cpp b/src/widgets/settingspages/SettingWidget.cpp index c4d5a8d5..6eaaf9cc 100644 --- a/src/widgets/settingspages/SettingWidget.cpp +++ b/src/widgets/settingspages/SettingWidget.cpp @@ -1,10 +1,15 @@ #include "widgets/settingspages/SettingWidget.hpp" +#include "util/RapidJsonSerializeQString.hpp" // IWYU pragma: keep +#include "widgets/dialogs/ColorPickerDialog.hpp" +#include "widgets/helper/color/ColorButton.hpp" #include "widgets/settingspages/GeneralPageView.hpp" #include #include +#include #include +#include namespace { @@ -86,6 +91,145 @@ SettingWidget *SettingWidget::inverseCheckbox(const QString &label, return widget; } +SettingWidget *SettingWidget::customCheckbox( + const QString &label, bool initialValue, + const std::function &save) +{ + auto *widget = new SettingWidget(label); + + auto *check = new QCheckBox(label); + + widget->hLayout->addWidget(check); + + check->setChecked(initialValue); + + QObject::connect(check, &QCheckBox::toggled, widget, save); + + widget->actionWidget = check; + widget->label = check; + + return widget; +} + +SettingWidget *SettingWidget::intInput(const QString &label, + IntSetting &setting, + IntInputParams params) +{ + auto *widget = new SettingWidget(label); + + auto *lbl = new QLabel(label + ":"); + + auto *input = new QSpinBox; + if (params.min.has_value()) + { + input->setMinimum(params.min.value()); + } + if (params.max.has_value()) + { + input->setMaximum(params.max.value()); + } + if (params.singleStep.has_value()) + { + input->setSingleStep(params.singleStep.value()); + } + + widget->hLayout->addWidget(lbl); + widget->hLayout->addStretch(1); + widget->hLayout->addWidget(input); + + // update when setting changes + setting.connect( + [input](const int &value, const auto &) { + input->setValue(value); + }, + widget->managedConnections); + + // update setting on value changed + QObject::connect(input, QOverload::of(&QSpinBox::valueChanged), widget, + [&setting](int newValue) { + setting = newValue; + }); + + widget->actionWidget = input; + widget->label = lbl; + + return widget; +} + +SettingWidget *SettingWidget::colorButton(const QString &label, + QStringSetting &setting) +{ + QColor color(setting.getValue()); + auto *widget = new SettingWidget(label); + + auto *lbl = new QLabel(label + ":"); + + auto *colorButton = new ColorButton(color); + + widget->hLayout->addWidget(lbl); + widget->hLayout->addStretch(1); + widget->hLayout->addWidget(colorButton); + + // update when setting changes + setting.connect( + [colorButton](const QString &value, const auto &) { + colorButton->setColor(QColor(value)); + }, + widget->managedConnections); + + QObject::connect(colorButton, &ColorButton::clicked, [widget, &setting]() { + auto *dialog = new ColorPickerDialog(QColor(setting), widget); + // colorButton & setting are never deleted and the signal is deleted + // once the dialog is closed + QObject::connect( + dialog, &ColorPickerDialog::colorConfirmed, widget, + [&setting](auto selected) { + if (selected.isValid()) + { + setting.setValue(selected.name(QColor::HexArgb)); + } + }); + dialog->show(); + }); + + widget->actionWidget = colorButton; + widget->label = lbl; + + return widget; +} + +SettingWidget *SettingWidget::lineEdit(const QString &label, + QStringSetting &setting, + const QString &placeholderText) +{ + QColor color(setting.getValue()); + auto *widget = new SettingWidget(label); + + auto *lbl = new QLabel(label + ":"); + + auto *edit = new QLineEdit; + edit->setText(setting); + if (!placeholderText.isEmpty()) + { + edit->setPlaceholderText(placeholderText); + } + + widget->hLayout->addWidget(lbl); + // widget->hLayout->addStretch(1); + widget->hLayout->addWidget(edit); + + // update when setting changes + QObject::connect(edit, &QLineEdit::textChanged, + [&setting](const QString &newValue) { + setting = newValue; + }); + + widget->actionWidget = edit; + widget->label = lbl; + + return widget; +} + SettingWidget *SettingWidget::setTooltip(QString tooltip) { assert(!tooltip.isEmpty()); @@ -136,9 +280,37 @@ SettingWidget *SettingWidget::addKeywords(const QStringList &newKeywords) return this; } +SettingWidget *SettingWidget::conditionallyEnabledBy(BoolSetting &setting) +{ + setting.connect( + [this](const auto &value, const auto &) { + this->actionWidget->setEnabled(value); + }, + this->managedConnections); + + return this; +} + void SettingWidget::addTo(GeneralPageView &view) { - view.addWidget(this, this->keywords); + view.pushWidget(this); + + if (this->label != nullptr) + { + view.registerWidget(this->label, this->keywords, this); + } + view.registerWidget(this->actionWidget, this->keywords, this); +} + +void SettingWidget::addTo(GeneralPageView &view, QFormLayout *formLayout) +{ + if (this->label != nullptr) + { + view.registerWidget(this->label, this->keywords, this); + } + view.registerWidget(this->actionWidget, this->keywords, this); + + formLayout->addRow(this->label, this->actionWidget); } } // namespace chatterino diff --git a/src/widgets/settingspages/SettingWidget.hpp b/src/widgets/settingspages/SettingWidget.hpp index b558b18a..e0e37a7b 100644 --- a/src/widgets/settingspages/SettingWidget.hpp +++ b/src/widgets/settingspages/SettingWidget.hpp @@ -15,6 +15,8 @@ #include #include +class QFormLayout; + namespace chatterino { class GeneralPageView; @@ -26,6 +28,12 @@ class SettingWidget : QWidget explicit SettingWidget(const QString &mainKeyword); public: + struct IntInputParams { + std::optional min; + std::optional max; + std::optional singleStep; + }; + ~SettingWidget() override = default; SettingWidget &operator=(const SettingWidget &) = delete; SettingWidget &operator=(SettingWidget &&) = delete; @@ -35,6 +43,13 @@ public: static SettingWidget *checkbox(const QString &label, BoolSetting &setting); static SettingWidget *inverseCheckbox(const QString &label, BoolSetting &setting); + static SettingWidget *customCheckbox(const QString &label, + bool initialValue, + const std::function &save); + + static SettingWidget *intInput(const QString &label, IntSetting &setting, + IntInputParams params); + template static SettingWidget *dropdown(const QString &label, EnumStringSetting &setting) @@ -81,6 +96,11 @@ public: return widget; } + static SettingWidget *colorButton(const QString &label, + QStringSetting &setting); + static SettingWidget *lineEdit(const QString &label, + QStringSetting &setting, + const QString &placeholderText = {}); SettingWidget *setTooltip(QString tooltip); SettingWidget *setDescription(const QString &text); @@ -90,7 +110,10 @@ public: /// All text from the tooltip, description, and label are already keywords SettingWidget *addKeywords(const QStringList &newKeywords); + SettingWidget *conditionallyEnabledBy(BoolSetting &setting); + void addTo(GeneralPageView &view); + void addTo(GeneralPageView &view, QFormLayout *formLayout); private: QWidget *label = nullptr;