refactor: use some more SettingWidget & restyle external tools page (#6023)

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