Compare commits

...

6 commits

Author SHA1 Message Date
pajlada cc0820c2a4
Merge f54bdf939a into 2ec8fa8723 2024-10-21 19:22:31 +02:00
pajlada 2ec8fa8723
refactor: remove unused ReplyContext.highlight (#5669) 2024-10-21 19:22:23 +02:00
pajlada 45d2c292d0
fix: subscribed threads not being marked as subscribed (#5668) 2024-10-21 13:19:08 +02:00
Rasmus Karlsson f54bdf939a
add changelog entry 2024-09-08 14:54:02 +02:00
Rasmus Karlsson adf124c11b
feat: add SettingWidget
bit more like a builder pattern for adding settings to settings pages

i have migrated a few settings, but don't want to do all of them
2024-09-08 14:52:11 +02:00
Rasmus Karlsson 74101d40f5
fix: only require one of the keywords to match when searching for
settings
2024-09-08 14:30:16 +02:00
10 changed files with 336 additions and 126 deletions

View file

@ -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)
@ -110,7 +111,7 @@
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
- Dev: Move plugins to Sol2. (#5622)
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660)
- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668)
- Dev: Refactored IRC message building. (#5663)
## 2.5.1

View file

@ -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;

View file

@ -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

View file

@ -2382,6 +2382,11 @@ void MessageBuilder::parseThread(const QString &messageContent,
this->message().replyParent = parent;
thread->addToThread(std::weak_ptr{this->message_});
if (thread->subscribed())
{
this->message().flags.set(MessageFlag::SubscribedThread);
}
// enable reply flag
this->message().flags.set(MessageFlag::ReplyMessage);

View file

@ -123,47 +123,34 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content)
return 0;
}
[[nodiscard]] bool shouldHighlightReplyThread(
const QVariantMap &tags, const QString &senderLogin,
std::shared_ptr<MessageThread> &thread, bool isNew)
void checkThreadSubscription(const QVariantMap &tags,
const QString &senderLogin,
std::shared_ptr<MessageThread> &thread)
{
const auto &currentLogin =
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (thread->subscribed())
if (thread->subscribed() || thread->unsubscribed())
{
return true;
}
if (thread->unsubscribed())
{
return false;
return;
}
if (getSettings()->autoSubToParticipatedThreads)
{
if (isNew)
const auto &currentLogin =
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (senderLogin == currentLogin)
{
if (const auto it = tags.find("reply-parent-user-login");
thread->markSubscribed();
}
else if (const auto it = tags.find("reply-parent-user-login");
it != tags.end())
{
auto name = it.value().toString();
if (name == currentLogin)
{
thread->markSubscribed();
return true; // already marked as participated
}
}
}
if (senderLogin == currentLogin)
{
thread->markSubscribed();
// don't set the highlight here
}
}
return false;
}
ChannelPtr channelOrEmptyByTarget(const QString &target,
@ -243,7 +230,6 @@ QMap<QString, QString> parseBadges(const QString &badgesString)
struct ReplyContext {
std::shared_ptr<MessageThread> thread;
MessagePtr parent;
bool highlight = false;
};
[[nodiscard]] ReplyContext getReplyContext(
@ -265,8 +251,7 @@ struct ReplyContext {
if (owned)
{
// Thread already exists (has a reply)
ctx.highlight = shouldHighlightReplyThread(
tags, message->nick(), owned, false);
checkThreadSubscription(tags, message->nick(), owned);
ctx.thread = owned;
rootThread = owned;
}
@ -301,8 +286,7 @@ struct ReplyContext {
{
std::shared_ptr<MessageThread> newThread =
std::make_shared<MessageThread>(foundMessage);
ctx.highlight = shouldHighlightReplyThread(
tags, message->nick(), newThread, true);
checkThreadSubscription(tags, message->nick(), newThread);
ctx.thread = newThread;
rootThread = newThread;
@ -724,10 +708,6 @@ std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
if (built)
{
if (replyCtx.highlight)
{
built->flags.set(MessageFlag::SubscribedThread);
}
builtMessages.emplace_back(built);
MessageBuilder::triggerHighlights(channel, alert);
}
@ -1552,8 +1532,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{
// Thread already exists (has a reply)
auto thread = threadIt->second.lock();
replyCtx.highlight = shouldHighlightReplyThread(
tags, message->nick(), thread, false);
checkThreadSubscription(tags, message->nick(), thread);
replyCtx.thread = thread;
rootThread = thread;
}
@ -1565,8 +1544,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{
// Found root reply message
auto newThread = std::make_shared<MessageThread>(root);
replyCtx.highlight = shouldHighlightReplyThread(
tags, message->nick(), newThread, true);
checkThreadSubscription(tags, message->nick(), newThread);
replyCtx.thread = newThread;
rootThread = newThread;
@ -1621,10 +1599,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
msg->flags.set(MessageFlag::Subscription);
msg->flags.unset(MessageFlag::Highlighted);
}
if (replyCtx.highlight)
{
msg->flags.set(MessageFlag::SubscribedThread);
}
IrcMessageHandler::setSimilarityFlags(msg, chan);

View file

@ -20,6 +20,7 @@
#include "util/IncognitoBrowser.hpp"
#include "widgets/BaseWindow.hpp"
#include "widgets/settingspages/GeneralPageView.hpp"
#include "widgets/settingspages/SettingWidget.hpp"
#include <magic_enum/magic_enum.hpp>
#include <QDesktopServices>
@ -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 <a href="https://pr.alejo.io">pr.alejo.io</a> 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<ChatSendProtocol>(
"Chat send protocol", qmagicenum::enumNames<ChatSendProtocol>(),
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<SoundBackend>(
"Sound backend (requires restart)",
qmagicenum::enumNames<SoundBackend>(), 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();

View file

@ -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 <QRegularExpression>
#include <QScrollArea>
@ -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,13 +384,12 @@ bool GeneralPageView::filterElements(const QString &query)
currentSubtitleVisible = true;
widget.element->show();
groupAny = true;
break;
}
else
{
widget.element->hide();
}
}
}
if (currentSubtitle)
{

View file

@ -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 <typename T, std::size_t N>
ComboBox *addDropdownEnumClass(const QString &text,
const std::array<QStringView, N> &items,
EnumStringSetting<T> &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<T>(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]() {

View file

@ -0,0 +1,144 @@
#include "widgets/settingspages/SettingWidget.hpp"
#include "widgets/settingspages/GeneralPageView.hpp"
#include <QBoxLayout>
#include <QCheckBox>
#include <QLabel>
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

View file

@ -0,0 +1,106 @@
#pragma once
#include "common/ChatterinoSetting.hpp"
#include "util/QMagicEnum.hpp"
#include "widgets/settingspages/GeneralPageView.hpp"
#include <pajlada/signals/signalholder.hpp>
#include <QBoxLayout>
#include <QComboBox>
#include <QLabel>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QtContainerFwd>
#include <QWidget>
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 <typename T>
static SettingWidget *dropdown(const QString &label,
EnumStringSetting<T> &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<T>())
{
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<T>(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