From fc61e8d64d95f2bfb826b798aca85e6f577dabc4 Mon Sep 17 00:00:00 2001 From: Arne <78976058+4rneee@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:36:58 +0100 Subject: [PATCH 01/33] feat: add /announce[color] commands (#5250) --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 4 ++ .../commands/builtin/twitch/Announce.cpp | 67 +++++++++++++++++-- .../commands/builtin/twitch/Announce.hpp | 12 ++++ 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c9d449be7..0498968456a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231) - Minor: Add permissions to experimental plugins feature. (#5231) +- Minor: Add support to send /announce[color] commands. (#5250) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 35cd4be0108..a5554570a87 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -401,6 +401,10 @@ void CommandController::initialize(Settings &, const Paths &paths) this->registerCommand("/unmod", &commands::removeModerator); this->registerCommand("/announce", &commands::sendAnnouncement); + this->registerCommand("/announceblue", &commands::sendAnnouncementBlue); + this->registerCommand("/announcegreen", &commands::sendAnnouncementGreen); + this->registerCommand("/announceorange", &commands::sendAnnouncementOrange); + this->registerCommand("/announcepurple", &commands::sendAnnouncementPurple); this->registerCommand("/vip", &commands::addVIP); diff --git a/src/controllers/commands/builtin/twitch/Announce.cpp b/src/controllers/commands/builtin/twitch/Announce.cpp index 566c79fe10a..d14d1f914b3 100644 --- a/src/controllers/commands/builtin/twitch/Announce.cpp +++ b/src/controllers/commands/builtin/twitch/Announce.cpp @@ -9,9 +9,11 @@ #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" -namespace chatterino::commands { +namespace { +using namespace chatterino; -QString sendAnnouncement(const CommandContext &ctx) +QString sendAnnouncementColor(const CommandContext &ctx, + const HelixAnnouncementColor color) { if (ctx.channel == nullptr) { @@ -25,11 +27,32 @@ QString sendAnnouncement(const CommandContext &ctx) return ""; } + QString colorStr = ""; + if (color != HelixAnnouncementColor::Primary) + { + colorStr = + QString::fromStdString( + std::string{ + magic_enum::enum_name(color)}) + .toLower(); + } + if (ctx.words.size() < 2) { - ctx.channel->addMessage(makeSystemMessage( - "Usage: /announce - Call attention to your " - "message with a highlight.")); + QString usageMsg; + if (color == HelixAnnouncementColor::Primary) + { + usageMsg = "Usage: /announce - Call attention to your " + "message with a highlight."; + } + else + { + usageMsg = + QString("Usage: /announce%1 - Call attention to your " + "message with a %1 highlight.") + .arg(colorStr); + } + ctx.channel->addMessage(makeSystemMessage(usageMsg)); return ""; } @@ -37,13 +60,14 @@ QString sendAnnouncement(const CommandContext &ctx) if (user->isAnon()) { ctx.channel->addMessage(makeSystemMessage( - "You must be logged in to use the /announce command.")); + QString("You must be logged in to use the /announce%1 command.") + .arg(colorStr))); return ""; } getHelix()->sendChatAnnouncement( ctx.twitchChannel->roomId(), user->getUserId(), - ctx.words.mid(1).join(" "), HelixAnnouncementColor::Primary, + ctx.words.mid(1).join(" "), color, []() { // do nothing. }, @@ -78,4 +102,33 @@ QString sendAnnouncement(const CommandContext &ctx) return ""; } +} // namespace + +namespace chatterino::commands { + +QString sendAnnouncement(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Primary); +} + +QString sendAnnouncementBlue(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Blue); +} + +QString sendAnnouncementGreen(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Green); +} + +QString sendAnnouncementOrange(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Orange); +} + +QString sendAnnouncementPurple(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Purple); +} + } // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Announce.hpp b/src/controllers/commands/builtin/twitch/Announce.hpp index 3904d1a203c..898ea0e32d9 100644 --- a/src/controllers/commands/builtin/twitch/Announce.hpp +++ b/src/controllers/commands/builtin/twitch/Announce.hpp @@ -13,4 +13,16 @@ namespace chatterino::commands { /// /announce QString sendAnnouncement(const CommandContext &ctx); +/// /announceblue +QString sendAnnouncementBlue(const CommandContext &ctx); + +/// /announcegreen +QString sendAnnouncementGreen(const CommandContext &ctx); + +/// /announceorange +QString sendAnnouncementOrange(const CommandContext &ctx); + +/// /announcepurple +QString sendAnnouncementPurple(const CommandContext &ctx); + } // namespace chatterino::commands From 47c46b64eaaf5eb9689810e8cc88b4ce85f3b683 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 16 Mar 2024 13:03:57 +0100 Subject: [PATCH 02/33] fix(channel-view): use `underlyingChannel_` over `channel_` (#5248) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 72 ++++++++++++++++-------------- src/widgets/helper/ChannelView.hpp | 40 +++++++++++++++++ 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0498968456a..6462cc96031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ - Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) +- Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 0cac3913621..3a38cadf0d1 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -453,7 +453,8 @@ void ChannelView::initializeSignals() this->signalHolder_.managedConnect( getIApp()->getWindows()->layoutRequested, [&](Channel *channel) { if (this->isVisible() && - (channel == nullptr || this->channel_.get() == channel)) + (channel == nullptr || + this->underlyingChannel_.get() == channel)) { this->queueLayout(); } @@ -463,7 +464,8 @@ void ChannelView::initializeSignals() getIApp()->getWindows()->invalidateBuffersRequested, [this](Channel *channel) { if (this->isVisible() && - (channel == nullptr || this->channel_.get() == channel)) + (channel == nullptr || + this->underlyingChannel_.get() == channel)) { this->invalidateBuffers(); } @@ -975,6 +977,41 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->channel_->fillInMissingMessages(filtered); }); + // Copy over messages from the backing channel to the filtered one + // and the ui. + auto snapshot = underlyingChannel->getMessageSnapshot(); + + this->scrollBar_->setMaximum(qreal(snapshot.size())); + + for (const auto &msg : snapshot) + { + if (!this->shouldIncludeMessage(msg)) + { + continue; + } + + auto messageLayout = std::make_shared(msg); + + if (this->lastMessageHasAlternateBackground_) + { + messageLayout->flags.set(MessageLayoutFlag::AlternateBackground); + } + this->lastMessageHasAlternateBackground_ = + !this->lastMessageHasAlternateBackground_; + + if (underlyingChannel->shouldIgnoreHighlights()) + { + messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights); + } + + this->messages_.pushBack(messageLayout); + this->channel_->addMessage(msg); + if (this->showScrollbarHighlights()) + { + this->scrollBar_->addHighlight(msg->getScrollBarHighlight()); + } + } + // // Standard channel connections // @@ -1006,33 +1043,6 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->messagesUpdated(); }); - auto snapshot = underlyingChannel->getMessageSnapshot(); - - this->scrollBar_->setMaximum(qreal(snapshot.size())); - - for (const auto &msg : snapshot) - { - auto messageLayout = std::make_shared(msg); - - if (this->lastMessageHasAlternateBackground_) - { - messageLayout->flags.set(MessageLayoutFlag::AlternateBackground); - } - this->lastMessageHasAlternateBackground_ = - !this->lastMessageHasAlternateBackground_; - - if (underlyingChannel->shouldIgnoreHighlights()) - { - messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights); - } - - this->messages_.pushBack(messageLayout); - if (this->showScrollbarHighlights()) - { - this->scrollBar_->addHighlight(msg->getScrollBarHighlight()); - } - } - this->underlyingChannel_ = underlyingChannel; this->performLayout(); @@ -2991,10 +3001,6 @@ void ChannelView::setInputReply(const MessagePtr &message) // Message did not already have a thread attached, try to find or create one auto *tc = dynamic_cast(this->underlyingChannel_.get()); - if (!tc) - { - tc = dynamic_cast(this->channel_.get()); - } if (tc) { diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 50156c5e16f..e6cb7597ee8 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -139,15 +139,32 @@ class ChannelView final : public BaseWidget MessageElementFlags getFlags() const; + /// @brief The virtual channel used to display messages + /// + /// This channel contains all messages in this view and respects the + /// filter settings. It will always be of type Channel, not TwitchChannel + /// nor IrcChannel. + /// It's **not** equal to the channel passed in #setChannel(). ChannelPtr channel(); + + /// Set the channel this view is displaying void setChannel(const ChannelPtr &underlyingChannel); void setFilters(const QList &ids); QList getFilterIds() const; FilterSetPtr getFilterSet() const; + /// @brief The channel this is derived from + /// + /// In case of "nested" channel views such as in user popups, + /// this channel is set to the original channel the messages came from, + /// which is used to open user popups from this view. + /// It's not always set. + /// @see #hasSourceChannel() ChannelPtr sourceChannel() const; + /// Setter for #sourceChannel() void setSourceChannel(ChannelPtr sourceChannel); + /// Checks if this view has a #sourceChannel bool hasSourceChannel() const; LimitedQueueSnapshot &getMessagesSnapshot(); @@ -300,8 +317,31 @@ class ChannelView final : public BaseWidget ThreadGuard snapshotGuard_; LimitedQueueSnapshot snapshot_; + /// @brief The backing (internal) channel + /// + /// This is a "virtual" channel where all filtered messages from + /// @a underlyingChannel_ are added to. It contains messages visible on + /// screen and will always be a @a Channel, or, it will never be a + /// TwitchChannel or IrcChannel, however, it will have the same type and + /// name as @a underlyingChannel_. It's not know to any registry/server. ChannelPtr channel_ = nullptr; + + /// @brief The channel receiving messages + /// + /// This channel is the one passed in #setChannel(). It's known to the + /// respective registry (e.g. TwitchIrcServer). For Twitch channels for + /// example, this will be an instance of TwitchChannel. This channel might + /// contain more messages than visible if filters are active. ChannelPtr underlyingChannel_ = nullptr; + + /// @brief The channel @a underlyingChannel_ is derived from + /// + /// In case of "nested" channel views such as in user popups, + /// this channel is set to the original channel the messages came from, + /// which is used to open user popups from this view. + /// + /// @see #sourceChannel() + /// @see #hasSourceChannel() ChannelPtr sourceChannel_ = nullptr; Split *split_; From 3563ecb3a5ec1991e3364ada816a0e23f6d639d7 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 16 Mar 2024 15:15:05 +0100 Subject: [PATCH 03/33] fix: Compile Lua as a C library (#5251) --- CHANGELOG.md | 1 + lib/lua/CMakeLists.txt | 2 +- src/controllers/plugins/LuaAPI.cpp | 2 ++ src/controllers/plugins/LuaAPI.hpp | 2 ++ src/controllers/plugins/LuaUtilities.cpp | 2 ++ src/controllers/plugins/LuaUtilities.hpp | 2 ++ src/controllers/plugins/Plugin.cpp | 2 ++ src/controllers/plugins/PluginController.cpp | 2 ++ src/controllers/plugins/api/ChannelRef.cpp | 2 ++ src/controllers/plugins/api/IOWrapper.cpp | 7 +++++-- 10 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6462cc96031..ce88f1f8f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,7 @@ - Dev: Load less message history upon reconnects. (#5001, #5018) - Dev: Removed the `NullablePtr` class. (#5091) - Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014, #5108) +- Dev: Compile Lua as a C library. (#5251) - Dev: Fixed most compiler warnings. (#5028, #5137) - Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747) - Dev: Refactor Args to be less of a singleton. (#5041) diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt index 086f5949511..cf2fad9bd11 100644 --- a/lib/lua/CMakeLists.txt +++ b/lib/lua/CMakeLists.txt @@ -50,4 +50,4 @@ target_include_directories(lua PUBLIC ${LUA_INCLUDE_DIRS} ) -set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX) +set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C) diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index 291e95b22cd..497c25260ea 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -9,9 +9,11 @@ # include "messages/MessageBuilder.hpp" # include "providers/twitch/TwitchIrcServer.hpp" +extern "C" { # include # include # include +} # include # include # include diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index c37cfb7ef51..df042b24f5c 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -2,7 +2,9 @@ #ifdef CHATTERINO_HAVE_PLUGINS +extern "C" { # include +} # include # include diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index 6866c2cc0f8..9361cd1ff3e 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -7,8 +7,10 @@ # include "controllers/plugins/api/ChannelRef.hpp" # include "controllers/plugins/LuaAPI.hpp" +extern "C" { # include # include +} # include # include diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index f610ae25d32..4c78d6edc9a 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -4,8 +4,10 @@ # include "common/QLogging.hpp" +extern "C" { # include # include +} # include # include diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index 562b6c07b6a..fc0a255cc08 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -4,7 +4,9 @@ # include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" +extern "C" { # include +} # include # include # include diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 87683ce0eeb..0f23df3430d 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -14,9 +14,11 @@ # include "singletons/Paths.hpp" # include "singletons/Settings.hpp" +extern "C" { # include # include # include +} # include # include diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index 8ae91cd97c9..986fbbac359 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -9,8 +9,10 @@ # include "providers/twitch/TwitchChannel.hpp" # include "providers/twitch/TwitchIrcServer.hpp" +extern "C" { # include # include +} # include # include diff --git a/src/controllers/plugins/api/IOWrapper.cpp b/src/controllers/plugins/api/IOWrapper.cpp index 7eeffaf71d7..f6a58a0bb1b 100644 --- a/src/controllers/plugins/api/IOWrapper.cpp +++ b/src/controllers/plugins/api/IOWrapper.cpp @@ -4,8 +4,11 @@ # include "Application.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/PluginController.hpp" -# include "lauxlib.h" -# include "lua.h" + +extern "C" { +# include +# include +} # include From 0322d37650c9b5b3b644ecb22f9c1e54b619bef0 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 17 Mar 2024 12:21:15 +0100 Subject: [PATCH 04/33] Show line indicator instead of rectangle while dragging in tables (#5252) --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/widgets/helper/EditableModelView.cpp | 3 ++ src/widgets/helper/TableStyles.cpp | 66 ++++++++++++++++++++++++ src/widgets/helper/TableStyles.hpp | 32 ++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 src/widgets/helper/TableStyles.cpp create mode 100644 src/widgets/helper/TableStyles.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ce88f1f8f11..9a2930dd6bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231) - Minor: Add permissions to experimental plugins feature. (#5231) - Minor: Add support to send /announce[color] commands. (#5250) +- Minor: Added drop indicator line while dragging in tables. (#5252) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba5e85b4d14..24853e75271 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -651,6 +651,8 @@ set(SOURCE_FILES widgets/helper/SettingsDialogTab.hpp widgets/helper/SignalLabel.cpp widgets/helper/SignalLabel.hpp + widgets/helper/TableStyles.cpp + widgets/helper/TableStyles.hpp widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButtons.cpp diff --git a/src/widgets/helper/EditableModelView.cpp b/src/widgets/helper/EditableModelView.cpp index b549c9e9ba5..cbc04c03b32 100644 --- a/src/widgets/helper/EditableModelView.cpp +++ b/src/widgets/helper/EditableModelView.cpp @@ -1,6 +1,7 @@ #include "EditableModelView.hpp" #include "widgets/helper/RegExpItemDelegate.hpp" +#include "widgets/helper/TableStyles.hpp" #include #include @@ -28,6 +29,8 @@ EditableModelView::EditableModelView(QAbstractTableModel *model, bool movable) this->tableView_->verticalHeader()->setVisible(false); this->tableView_->horizontalHeader()->setSectionsClickable(false); + TableRowDragStyle::applyTo(this->tableView_); + // create layout QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(0, 0, 0, 0); diff --git a/src/widgets/helper/TableStyles.cpp b/src/widgets/helper/TableStyles.cpp new file mode 100644 index 00000000000..27ace07a9ba --- /dev/null +++ b/src/widgets/helper/TableStyles.cpp @@ -0,0 +1,66 @@ +#include "widgets/helper/TableStyles.hpp" + +#include +#include +#include +#include +#include + +namespace chatterino { + +TableRowDragStyle::TableRowDragStyle(QStyle *target) + : QProxyStyle(target) +{ +} + +void TableRowDragStyle::applyTo(QTableView *view) +{ + auto *proxyStyle = new TableRowDragStyle(view->style()); + proxyStyle->setParent(view); + view->setStyle(proxyStyle); +} + +void TableRowDragStyle::drawPrimitive(QStyle::PrimitiveElement element, + const QStyleOption *option, + QPainter *painter, + const QWidget *widget) const +{ + if (element != QStyle::PE_IndicatorItemViewItemDrop) + { + QProxyStyle::drawPrimitive(element, option, painter, widget); + return; + } + + const auto *view = dynamic_cast(widget); + if (!view) + { + assert(false && "TableStyle must be used on a QAbstractItemView"); + return; + } + + if (option->rect.isNull()) + { + return; + } + + // Get the direction a row is dragged in + auto selected = view->currentIndex(); + auto hovered = view->indexAt(option->rect.center()); + if (!selected.isValid() || !hovered.isValid()) + { + // This shouldn't happen as we're in a drag operation + assert(false && "Got bad indices"); + return; + } + + int y = option->rect.top(); // move up + if (hovered.row() >= selected.row()) + { + y = option->rect.bottom(); // move down + } + + painter->setPen({Qt::white, 2}); + painter->drawLine(0, y, widget->width(), y); +} + +} // namespace chatterino diff --git a/src/widgets/helper/TableStyles.hpp b/src/widgets/helper/TableStyles.hpp new file mode 100644 index 00000000000..c71d889db26 --- /dev/null +++ b/src/widgets/helper/TableStyles.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +class QTableView; + +namespace chatterino { + +/// @brief A custom style for drag operations of rows on tables +/// +/// This style overwrites how `PE_IndicatorItemViewItemDrop`, the drop +/// indicator of item-views, is drawn. It's intended to be used on QTableViews +/// where entire rows are moved (not individual cells). The indicator is shown +/// as a line at the position where the dragged item should be inserted. If no +/// such position exists, a red border is drawn around the viewport. +class TableRowDragStyle : public QProxyStyle +{ +public: + /// Applies the style to @a view + static void applyTo(QTableView *view); + + void drawPrimitive(QStyle::PrimitiveElement element, + const QStyleOption *option, QPainter *painter, + const QWidget *widget = nullptr) const override; + +private: + /// @param target The style to wrap. This is **not** the parent of this + /// object. This object will become the parent of @a target. + TableRowDragStyle(QStyle *target); +}; + +} // namespace chatterino From 46c5609736afcae4b6548fc7f605b39e2000887e Mon Sep 17 00:00:00 2001 From: askepticaldreamer <106888785+askepticaldreamer@users.noreply.github.com> Date: Sun, 17 Mar 2024 04:46:58 -0700 Subject: [PATCH 05/33] feat: Warn for commands with duplicate triggers (#4322) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/common/SignalVectorModel.hpp | 10 +++ src/widgets/settingspages/CommandPage.cpp | 101 +++++++++++++++++++--- src/widgets/settingspages/CommandPage.hpp | 1 - 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2930dd6bb..19dcbe7a753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231) - Minor: Add permissions to experimental plugins feature. (#5231) +- Minor: Added warning message if you have multiple commands with the same trigger. (#4322) - Minor: Add support to send /announce[color] commands. (#5250) - Minor: Added drop indicator line while dragging in tables. (#5252) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index bf31dbb00ae..620ca452d0c 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -165,12 +165,22 @@ class SignalVectorModel : public QAbstractTableModel, else { int vecRow = this->getVectorIndexFromModelIndex(row); + // TODO: This is only a safety-thing for when we modify data that's being modified right now. + // It should not be necessary, but it would require some rethinking about this surrounding logic + if (vecRow >= this->vector_->readOnly()->size()) + { + return false; + } this->vector_->removeAt(vecRow, this); assert(this->rows_[row].original); TVectorItem item = this->getItemFromRow( this->rows_[row].items, this->rows_[row].original.value()); this->vector_->insert(item, vecRow, this); + + QVector roles = QVector(); + roles.append(role); + emit dataChanged(index, index, roles); } return true; diff --git a/src/widgets/settingspages/CommandPage.cpp b/src/widgets/settingspages/CommandPage.cpp index a1aba057500..9dc53b9bf19 100644 --- a/src/widgets/settingspages/CommandPage.cpp +++ b/src/widgets/settingspages/CommandPage.cpp @@ -11,6 +11,7 @@ #include "util/StandardItemHelper.hpp" #include "widgets/helper/EditableModelView.hpp" +#include #include #include #include @@ -22,26 +23,73 @@ #define TEXT "{1} => first word     {1+} => first word and after     {{ => {     more info" // clang-format on -namespace chatterino { namespace { - QString c1settingsPath() + +using namespace chatterino; + +QString c1settingsPath() +{ + return combinePath(qgetenv("appdata"), "Chatterino\\Custom\\Commands.txt"); +} + +void checkCommandDuplicates(EditableModelView *view, QLabel *duplicateWarning) +{ + bool foundDuplicateTrigger = false; + + // Maps command triggers to model row indices + std::unordered_map> commands; + + for (int i = 0; i < view->getModel()->rowCount(); i++) + { + QString commandTrigger = + view->getModel()->index(i, 0).data().toString(); + commands[commandTrigger].push_back(i); + } + + for (const auto &[commandTrigger, rowIndices] : commands) { - return combinePath(qgetenv("appdata"), - "Chatterino\\Custom\\Commands.txt"); + assert(!rowIndices.empty()); + + if (rowIndices.size() > 1) + { + foundDuplicateTrigger = true; + + for (const auto &rowIndex : rowIndices) + { + view->getModel()->setData(view->getModel()->index(rowIndex, 0), + QColor("yellow"), Qt::ForegroundRole); + } + } + else + { + view->getModel()->setData(view->getModel()->index(rowIndices[0], 0), + QColor("white"), Qt::ForegroundRole); + } } + + if (foundDuplicateTrigger) + { + duplicateWarning->show(); + } + else + { + duplicateWarning->hide(); + } +} + } // namespace +namespace chatterino { + CommandPage::CommandPage() { - auto *app = getApp(); - LayoutCreator layoutCreator(this); auto layout = layoutCreator.setLayoutType(); - EditableModelView *view = layout - .emplace( - app->getCommands()->createModel(nullptr)) - .getElement(); + auto *view = layout + .emplace( + getIApp()->getCommands()->createModel(nullptr)) + .getElement(); view->setTitles({"Trigger", "Command", "Show In\nMessage Menu"}); view->getTableView()->horizontalHeader()->setSectionResizeMode( @@ -83,6 +131,39 @@ CommandPage::CommandPage() text->setStyleSheet("color: #bbb"); text->setOpenExternalLinks(true); + auto *duplicateWarning = + layout + .emplace("Multiple commands with the same trigger found. " + "Only one of the commands will work.") + .getElement(); + duplicateWarning->setStyleSheet("color: yellow"); + + // NOTE: These signals mean that the duplicate check happens in the middle of a row being moved, where he index can be wrong. + // This should be reconsidered, or potentially changed in the signalvectormodel. Or maybe we rely on a SignalVectorModel signal instead + QObject::connect(view->getModel(), &QAbstractItemModel::rowsInserted, this, + [view, duplicateWarning]() { + checkCommandDuplicates(view, duplicateWarning); + }); + + QObject::connect(view->getModel(), &QAbstractItemModel::rowsRemoved, this, + [view, duplicateWarning]() { + checkCommandDuplicates(view, duplicateWarning); + }); + + QObject::connect(view->getModel(), &QAbstractItemModel::dataChanged, this, + [view, duplicateWarning](const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector &roles) { + (void)topLeft; + (void)bottomRight; + if (roles.contains(Qt::EditRole)) + { + checkCommandDuplicates(view, duplicateWarning); + } + }); + + checkCommandDuplicates(view, duplicateWarning); + // ---- end of layout this->commandsEditTimer_.setSingleShot(true); } diff --git a/src/widgets/settingspages/CommandPage.hpp b/src/widgets/settingspages/CommandPage.hpp index ea97440bdf4..d88c00a615b 100644 --- a/src/widgets/settingspages/CommandPage.hpp +++ b/src/widgets/settingspages/CommandPage.hpp @@ -2,7 +2,6 @@ #include "widgets/settingspages/SettingsPage.hpp" -#include #include namespace chatterino { From c10e364e061ecc13f3cdeef82c57847b57e902ce Mon Sep 17 00:00:00 2001 From: KleberPF <43550602+KleberPF@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:43:55 -0300 Subject: [PATCH 06/33] Fix double click to select full words (#5243) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/messages/MessageElement.cpp | 5 +- src/messages/layouts/MessageLayout.cpp | 21 ++++- src/messages/layouts/MessageLayout.hpp | 16 +++- .../layouts/MessageLayoutContainer.cpp | 50 +++++++++++ .../layouts/MessageLayoutContainer.hpp | 20 +++++ src/messages/layouts/MessageLayoutElement.cpp | 10 +++ src/messages/layouts/MessageLayoutElement.hpp | 10 +++ src/widgets/helper/ChannelView.cpp | 22 +---- tests/CMakeLists.txt | 1 + tests/src/MessageLayout.cpp | 90 +++++++++++++++++++ 11 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 tests/src/MessageLayout.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 19dcbe7a753..7cac0b0f8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ - Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110) - Bugfix: Fixed emotes being reloaded when pressing "Cancel" in the settings dialog, causing a slowdown. (#5240) - Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126) +- Bugfix: Fixed double-click selection not selecting words that were split onto multiple lines correctly. (#5243) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126) - Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) - Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971) diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index e895f7630b2..56d1e2ed3d3 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -454,7 +454,7 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags, void TextElement::addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) { - auto *app = getApp(); + auto *app = getIApp(); if (flags.hasAny(this->getFlags())) { @@ -463,6 +463,8 @@ void TextElement::addToContainer(MessageLayoutContainer &container, for (const auto &word : this->words_) { + auto wordId = container.nextWordId(); + auto getTextLayoutElement = [&](QString text, int width, bool hasTrailingSpace) { auto color = this->color_.getColor(*app->getThemes()); @@ -473,6 +475,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, this->style_, container.getScale()); e->setTrailingSpace(hasTrailingSpace); e->setText(text); + e->setWordId(wordId); return e; }; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index efc1d1b561f..126bb40c94d 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -443,12 +443,31 @@ void MessageLayout::deleteCache() // returns nullptr if none was found // fourtf: this should return a MessageLayoutItem -const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) +const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) const { // go through all words and return the first one that contains the point. return this->container_.getElementAt(point); } +std::pair MessageLayout::getWordBounds( + const MessageLayoutElement *hoveredElement, QPoint relativePos) const +{ + // An element with wordId != -1 can be multiline, so we need to check all + // elements in the container + if (hoveredElement->getWordId() != -1) + { + return this->container_.getWordBounds(hoveredElement); + } + + const auto wordStart = this->getSelectionIndex(relativePos) - + hoveredElement->getMouseOverIndex(relativePos); + const auto selectionLength = hoveredElement->getSelectionIndexCount(); + const auto length = hoveredElement->hasTrailingSpace() ? selectionLength - 1 + : selectionLength; + + return {wordStart, wordStart + length}; +} + size_t MessageLayout::getLastCharacterIndex() const { return this->container_.getLastCharacterIndex(); diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index 8a177227fec..f54f57d2a9a 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -70,7 +70,21 @@ class MessageLayout * * If no element is found at the given point, this returns a null pointer */ - const MessageLayoutElement *getElementAt(QPoint point); + const MessageLayoutElement *getElementAt(QPoint point) const; + + /** + * @brief Returns the word bounds of the given element + * + * The first value is the index of the first character in the word, + * the second value is the index of the character after the last character in the word. + * + * Given the word "abc" by itself, we would return (0, 3) + * + * V V + * "abc " + */ + std::pair getWordBounds( + const MessageLayoutElement *hoveredElement, QPoint relativePos) const; /** * Get the index of the last character in this message's container diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 17b9b795d4b..29d70e0a193 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -51,6 +51,7 @@ void MessageLayoutContainer::beginLayout(int width, float scale, this->textLineHeight_ = mediumFontMetrics.height(); this->spaceWidth_ = mediumFontMetrics.horizontalAdvance(' '); this->dotdotdotWidth_ = mediumFontMetrics.horizontalAdvance("..."); + this->currentWordId_ = 0; this->canAddMessages_ = true; this->isCollapsed_ = false; this->wasPrevReversed_ = false; @@ -456,6 +457,50 @@ size_t MessageLayoutContainer::getFirstMessageCharacterIndex() const return index; } +std::pair MessageLayoutContainer::getWordBounds( + const MessageLayoutElement *hoveredElement) const +{ + if (this->elements_.empty()) + { + return {0, 0}; + } + + size_t index = 0; + size_t wordStart = 0; + + for (; index < this->elements_.size(); index++) + { + const auto &element = this->elements_[index]; + if (element->getWordId() == hoveredElement->getWordId()) + { + break; + } + + wordStart += element->getSelectionIndexCount(); + } + + size_t wordEnd = wordStart; + + for (; index < this->elements_.size(); index++) + { + const auto &element = this->elements_[index]; + if (element->getWordId() != hoveredElement->getWordId()) + { + break; + } + + wordEnd += element->getSelectionIndexCount(); + } + + const auto *lastElementInSelection = this->elements_[index - 1].get(); + if (lastElementInSelection->hasTrailingSpace()) + { + wordEnd--; + } + + return {wordStart, wordEnd}; +} + size_t MessageLayoutContainer::getLastCharacterIndex() const { if (this->lines_.empty()) @@ -505,6 +550,11 @@ int MessageLayoutContainer::remainingWidth() const this->currentX_; } +int MessageLayoutContainer::nextWordId() +{ + return this->currentWordId_++; +} + void MessageLayoutContainer::addElement(MessageLayoutElement *element, const bool forceAdd, const int prevIndex) diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index be765da85d4..ed3c1a7a6b7 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -111,6 +111,20 @@ struct MessageLayoutContainer { */ size_t getFirstMessageCharacterIndex() const; + /** + * @brief Returns the word bounds of the given element + * + * The first value is the index of the first character in the word, + * the second value is the index of the character after the last character in the word. + * + * Given the word "abc" by itself, we would return (0, 3) + * + * V V + * "abc " + */ + std::pair getWordBounds( + const MessageLayoutElement *hoveredElement) const; + /** * Get the index of the last character in this message * This is the sum of all the characters in `elements_` @@ -154,6 +168,11 @@ struct MessageLayoutContainer { */ int remainingWidth() const; + /** + * Returns the id of the next word that can be added to this container + */ + int nextWordId(); + private: struct Line { /** @@ -272,6 +291,7 @@ struct MessageLayoutContainer { int spaceWidth_ = 4; int textLineHeight_ = 0; int dotdotdotWidth_ = 0; + int currentWordId_ = 0; bool canAddMessages_ = true; bool isCollapsed_ = false; bool wasPrevReversed_ = false; diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index ffa949d7fe4..31b7d4fe573 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -108,6 +108,16 @@ FlagsEnum MessageLayoutElement::getFlags() const return this->creator_.getFlags(); } +int MessageLayoutElement::getWordId() const +{ + return this->wordId_; +} + +void MessageLayoutElement::setWordId(int wordId) +{ + this->wordId_ = wordId; +} + // // IMAGE // diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index bbb45302fc2..de68a43f782 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -71,6 +71,9 @@ class MessageLayoutElement const QString &getText() const; FlagsEnum getFlags() const; + int getWordId() const; + void setWordId(int wordId); + protected: bool trailingSpace = true; @@ -83,6 +86,13 @@ class MessageLayoutElement * The line of the container this element is laid out at */ size_t line_{}; + + /// @brief ID of a word inside its container + /// + /// One word has exactly one ID that is used to identify elements created + /// from the same word (due to wrapping). + /// IDs are unique in a MessageLayoutContainer. + int wordId_ = -1; }; // IMAGE diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3a38cadf0d1..3cf64e1160c 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -290,23 +290,6 @@ qreal highlightEasingFunction(qreal progress) return 1.0 + pow((20.0 / 9.0) * (0.5 * progress - 0.5), 3.0); } -/// @return the start and end of the word bounds -std::pair getWordBounds(MessageLayout *layout, - const MessageLayoutElement *element, - const QPoint &relativePos) -{ - assert(layout != nullptr); - assert(element != nullptr); - - const auto wordStart = layout->getSelectionIndex(relativePos) - - element->getMouseOverIndex(relativePos); - const auto selectionLength = element->getSelectionIndexCount(); - const auto length = - element->hasTrailingSpace() ? selectionLength - 1 : selectionLength; - - return {wordStart, wordStart + length}; -} - } // namespace namespace chatterino { @@ -1827,7 +1810,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) if (this->isDoubleClick_ && hoverLayoutElement) { auto [wordStart, wordEnd] = - getWordBounds(layout.get(), hoverLayoutElement, relativePos); + layout->getWordBounds(hoverLayoutElement, relativePos); auto hoveredWord = Selection{SelectionItem(messageIndex, wordStart), SelectionItem(messageIndex, wordEnd)}; // combined selection spanning from initially selected word to hoveredWord @@ -2657,7 +2640,8 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) } auto [wordStart, wordEnd] = - getWordBounds(layout.get(), hoverLayoutElement, relativePos); + layout->getWordBounds(hoverLayoutElement, relativePos); + this->doubleClickSelection_ = {SelectionItem(messageIndex, wordStart), SelectionItem(messageIndex, wordEnd)}; this->setSelection(this->doubleClickSelection_); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 53ffd5e1ab9..f1f80cf0e7b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/NotebookTab.cpp ${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp # Add your new file above this line! ) diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp new file mode 100644 index 00000000000..9ce0c7f21e8 --- /dev/null +++ b/tests/src/MessageLayout.cpp @@ -0,0 +1,90 @@ +#include "messages/layouts/MessageLayout.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" + +#include +#include +#include + +#include + +using namespace chatterino; + +namespace { + +class MockApplication : mock::EmptyApplication +{ +public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + , fonts(this->settings) + , windowManager(this->paths_) + { + } + Theme *getThemes() override + { + return &this->theme; + } + + Fonts *getFonts() override + { + return &this->fonts; + } + + WindowManager *getWindows() override + { + return &this->windowManager; + } + + Settings settings; + Theme theme; + Fonts fonts; + WindowManager windowManager; +}; + +constexpr int WIDTH = 300; + +class MessageLayoutTest +{ +public: + // "aaaaaaaa bbbbbbbb cccccccc" + MessageLayoutTest(const QString &text) + { + MessageBuilder builder; + builder.append( + std::make_unique(text, MessageElementFlag::Text)); + this->layout = std::make_unique(builder.release()); + this->layout->layout(WIDTH, 1, MessageElementFlag::Text, false); + } + + MockApplication mockApplication; + std::unique_ptr layout; +}; + +} // namespace + +TEST(TextElement, BasicCase) +{ + auto test = MessageLayoutTest("abc"); + + // Simulate we are clicking on the first word + auto point = QPoint(WIDTH / 20, test.layout->getHeight() / 2); + + const auto *hoveredElement = test.layout->getElementAt(point); + ASSERT_NE(hoveredElement, nullptr); + + const auto [wordStart, wordEnd] = + test.layout->getWordBounds(hoveredElement, point); + + EXPECT_EQ(wordStart, 0); + EXPECT_EQ(wordEnd, 3); +} From dc989a9298f3d395042b90a4de24cd25eb7a7653 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 17 Mar 2024 15:20:19 +0100 Subject: [PATCH 07/33] fix: output less qt debug stuff in tests (#5253) --- tests/src/main.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 44a8015c394..3b24a997885 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -23,7 +23,7 @@ int main(int argc, char **argv) #ifdef SUPPORT_QT_NETWORK_TESTS QApplication app(argc, argv); // make sure to always debug-log - QLoggingCategory::setFilterRules("*.debug=true"); + QLoggingCategory::setFilterRules("chatterino.*=true"); initResources(); @@ -32,7 +32,6 @@ int main(int argc, char **argv) // Ensure settings are initialized before any tests are run QTemporaryDir settingsDir; settingsDir.setAutoRemove(false); // we'll remove it manually - qDebug() << "Settings directory:" << settingsDir.path(); chatterino::Settings settings(settingsDir.path()); QTimer::singleShot(0, [&]() { From f21b9a2daf257e7485a5a4712f74e333164801ea Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Sun, 17 Mar 2024 20:07:53 -0400 Subject: [PATCH 08/33] Revert "Show line indicator instead of rectangle while dragging in tables" (#5255) This reverts commit 0322d37650c9b5b3b644ecb22f9c1e54b619bef0. --- CHANGELOG.md | 1 - src/CMakeLists.txt | 2 - src/widgets/helper/EditableModelView.cpp | 3 -- src/widgets/helper/TableStyles.cpp | 66 ------------------------ src/widgets/helper/TableStyles.hpp | 32 ------------ 5 files changed, 104 deletions(-) delete mode 100644 src/widgets/helper/TableStyles.cpp delete mode 100644 src/widgets/helper/TableStyles.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cac0b0f8aa..56fd1309359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,6 @@ - Minor: Add permissions to experimental plugins feature. (#5231) - Minor: Added warning message if you have multiple commands with the same trigger. (#4322) - Minor: Add support to send /announce[color] commands. (#5250) -- Minor: Added drop indicator line while dragging in tables. (#5252) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 24853e75271..ba5e85b4d14 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -651,8 +651,6 @@ set(SOURCE_FILES widgets/helper/SettingsDialogTab.hpp widgets/helper/SignalLabel.cpp widgets/helper/SignalLabel.hpp - widgets/helper/TableStyles.cpp - widgets/helper/TableStyles.hpp widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButtons.cpp diff --git a/src/widgets/helper/EditableModelView.cpp b/src/widgets/helper/EditableModelView.cpp index cbc04c03b32..b549c9e9ba5 100644 --- a/src/widgets/helper/EditableModelView.cpp +++ b/src/widgets/helper/EditableModelView.cpp @@ -1,7 +1,6 @@ #include "EditableModelView.hpp" #include "widgets/helper/RegExpItemDelegate.hpp" -#include "widgets/helper/TableStyles.hpp" #include #include @@ -29,8 +28,6 @@ EditableModelView::EditableModelView(QAbstractTableModel *model, bool movable) this->tableView_->verticalHeader()->setVisible(false); this->tableView_->horizontalHeader()->setSectionsClickable(false); - TableRowDragStyle::applyTo(this->tableView_); - // create layout QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(0, 0, 0, 0); diff --git a/src/widgets/helper/TableStyles.cpp b/src/widgets/helper/TableStyles.cpp deleted file mode 100644 index 27ace07a9ba..00000000000 --- a/src/widgets/helper/TableStyles.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "widgets/helper/TableStyles.hpp" - -#include -#include -#include -#include -#include - -namespace chatterino { - -TableRowDragStyle::TableRowDragStyle(QStyle *target) - : QProxyStyle(target) -{ -} - -void TableRowDragStyle::applyTo(QTableView *view) -{ - auto *proxyStyle = new TableRowDragStyle(view->style()); - proxyStyle->setParent(view); - view->setStyle(proxyStyle); -} - -void TableRowDragStyle::drawPrimitive(QStyle::PrimitiveElement element, - const QStyleOption *option, - QPainter *painter, - const QWidget *widget) const -{ - if (element != QStyle::PE_IndicatorItemViewItemDrop) - { - QProxyStyle::drawPrimitive(element, option, painter, widget); - return; - } - - const auto *view = dynamic_cast(widget); - if (!view) - { - assert(false && "TableStyle must be used on a QAbstractItemView"); - return; - } - - if (option->rect.isNull()) - { - return; - } - - // Get the direction a row is dragged in - auto selected = view->currentIndex(); - auto hovered = view->indexAt(option->rect.center()); - if (!selected.isValid() || !hovered.isValid()) - { - // This shouldn't happen as we're in a drag operation - assert(false && "Got bad indices"); - return; - } - - int y = option->rect.top(); // move up - if (hovered.row() >= selected.row()) - { - y = option->rect.bottom(); // move down - } - - painter->setPen({Qt::white, 2}); - painter->drawLine(0, y, widget->width(), y); -} - -} // namespace chatterino diff --git a/src/widgets/helper/TableStyles.hpp b/src/widgets/helper/TableStyles.hpp deleted file mode 100644 index c71d889db26..00000000000 --- a/src/widgets/helper/TableStyles.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -class QTableView; - -namespace chatterino { - -/// @brief A custom style for drag operations of rows on tables -/// -/// This style overwrites how `PE_IndicatorItemViewItemDrop`, the drop -/// indicator of item-views, is drawn. It's intended to be used on QTableViews -/// where entire rows are moved (not individual cells). The indicator is shown -/// as a line at the position where the dragged item should be inserted. If no -/// such position exists, a red border is drawn around the viewport. -class TableRowDragStyle : public QProxyStyle -{ -public: - /// Applies the style to @a view - static void applyTo(QTableView *view); - - void drawPrimitive(QStyle::PrimitiveElement element, - const QStyleOption *option, QPainter *painter, - const QWidget *widget = nullptr) const override; - -private: - /// @param target The style to wrap. This is **not** the parent of this - /// object. This object will become the parent of @a target. - TableRowDragStyle(QStyle *target); -}; - -} // namespace chatterino From e4ea9af0046043c65bcdf3c62823c83fc25cd766 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:07:18 -0400 Subject: [PATCH 09/33] chore(deps): bump ZedThree/clang-tidy-review from 0.17.1 to 0.17.2 (#5257) --- .github/workflows/clang-tidy.yml | 4 ++-- .github/workflows/post-clang-tidy-review.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 111a91296cf..0f072a36442 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -119,7 +119,7 @@ jobs: - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.1 + uses: ZedThree/clang-tidy-review@v0.17.2 with: build_dir: build-clang-tidy config_file: ".clang-tidy" @@ -145,4 +145,4 @@ jobs: libbenchmark-dev - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.1 + uses: ZedThree/clang-tidy-review/upload@v0.17.2 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index ad1523523dc..e22b264f502 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -14,6 +14,6 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.1 + - uses: ZedThree/clang-tidy-review/post@v0.17.2 with: lgtm_comment_body: "" From 044d457d20c6eb086cbc2a433b6f3187fff66e95 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 23 Mar 2024 11:56:42 +0100 Subject: [PATCH 10/33] fix: create NetworkManager statics in `init()` (#5254) --- CHANGELOG.md | 1 + src/common/network/NetworkManager.cpp | 29 +++++++++++++++++++++------ src/common/network/NetworkManager.hpp | 4 ++-- src/common/network/NetworkPrivate.cpp | 2 +- src/common/network/NetworkTask.cpp | 18 ++++++++--------- tests/src/NetworkRequest.cpp | 24 +++++++++++----------- 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56fd1309359..9496106cc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,7 @@ - Dev: Cleaned up and optimized resources. (#5222) - Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) +- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) ## 2.4.6 diff --git a/src/common/network/NetworkManager.cpp b/src/common/network/NetworkManager.cpp index dfc9fe0a068..956c2e79f31 100644 --- a/src/common/network/NetworkManager.cpp +++ b/src/common/network/NetworkManager.cpp @@ -4,19 +4,36 @@ namespace chatterino { -QThread NetworkManager::workerThread; -QNetworkAccessManager NetworkManager::accessManager; +QThread *NetworkManager::workerThread = nullptr; +QNetworkAccessManager *NetworkManager::accessManager = nullptr; void NetworkManager::init() { - NetworkManager::accessManager.moveToThread(&NetworkManager::workerThread); - NetworkManager::workerThread.start(); + assert(!NetworkManager::workerThread); + assert(!NetworkManager::accessManager); + + NetworkManager::workerThread = new QThread; + NetworkManager::workerThread->start(); + + NetworkManager::accessManager = new QNetworkAccessManager; + NetworkManager::accessManager->moveToThread(NetworkManager::workerThread); } void NetworkManager::deinit() { - NetworkManager::workerThread.quit(); - NetworkManager::workerThread.wait(); + assert(NetworkManager::workerThread); + assert(NetworkManager::accessManager); + + if (NetworkManager::workerThread) + { + NetworkManager::workerThread->quit(); + NetworkManager::workerThread->wait(); + } + + delete NetworkManager::accessManager; + NetworkManager::accessManager = nullptr; + delete NetworkManager::workerThread; + NetworkManager::workerThread = nullptr; } } // namespace chatterino diff --git a/src/common/network/NetworkManager.hpp b/src/common/network/NetworkManager.hpp index 530aaae1f6c..b02ce04e58a 100644 --- a/src/common/network/NetworkManager.hpp +++ b/src/common/network/NetworkManager.hpp @@ -10,8 +10,8 @@ class NetworkManager : public QObject Q_OBJECT public: - static QThread workerThread; - static QNetworkAccessManager accessManager; + static QThread *workerThread; + static QNetworkAccessManager *accessManager; static void init(); static void deinit(); diff --git a/src/common/network/NetworkPrivate.cpp b/src/common/network/NetworkPrivate.cpp index adf46b6f702..51842dd578c 100644 --- a/src/common/network/NetworkPrivate.cpp +++ b/src/common/network/NetworkPrivate.cpp @@ -48,7 +48,7 @@ void loadUncached(std::shared_ptr &&data) NetworkRequester requester; auto *worker = new NetworkTask(std::move(data)); - worker->moveToThread(&NetworkManager::workerThread); + worker->moveToThread(NetworkManager::workerThread); QObject::connect(&requester, &NetworkRequester::requestUrl, worker, &NetworkTask::run); diff --git a/src/common/network/NetworkTask.cpp b/src/common/network/NetworkTask.cpp index 7590c8a4673..256743a4f8f 100644 --- a/src/common/network/NetworkTask.cpp +++ b/src/common/network/NetworkTask.cpp @@ -54,41 +54,41 @@ QNetworkReply *NetworkTask::createReply() { const auto &data = this->data_; const auto &request = this->data_->request; - auto &accessManager = NetworkManager::accessManager; + auto *accessManager = NetworkManager::accessManager; switch (this->data_->requestType) { case NetworkRequestType::Get: - return accessManager.get(request); + return accessManager->get(request); case NetworkRequestType::Put: - return accessManager.put(request, data->payload); + return accessManager->put(request, data->payload); case NetworkRequestType::Delete: - return accessManager.deleteResource(data->request); + return accessManager->deleteResource(data->request); case NetworkRequestType::Post: if (data->multiPartPayload) { assert(data->payload.isNull()); - return accessManager.post(request, - data->multiPartPayload.get()); + return accessManager->post(request, + data->multiPartPayload.get()); } else { - return accessManager.post(request, data->payload); + return accessManager->post(request, data->payload); } case NetworkRequestType::Patch: if (data->multiPartPayload) { assert(data->payload.isNull()); - return accessManager.sendCustomRequest( + return accessManager->sendCustomRequest( request, "PATCH", data->multiPartPayload.get()); } else { - return NetworkManager::accessManager.sendCustomRequest( + return NetworkManager::accessManager->sendCustomRequest( request, "PATCH", data->payload); } } diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index 7029488afdd..2f6b8102f96 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -74,7 +74,7 @@ TEST(NetworkRequest, Success) { const std::vector codes{200, 201, 202, 203, 204, 205, 206}; - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); for (const auto code : codes) { @@ -96,14 +96,14 @@ TEST(NetworkRequest, Success) waiter.waitForRequest(); } - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); } TEST(NetworkRequest, FinallyCallbackOnSuccess) { const std::vector codes{200, 201, 202, 203, 204, 205, 206}; - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); for (const auto code : codes) { @@ -132,7 +132,7 @@ TEST(NetworkRequest, Error) 411, 412, 413, 414, 418, 500, 501, 502, 503, 504, }; - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); for (const auto code : codes) { @@ -155,7 +155,7 @@ TEST(NetworkRequest, Error) waiter.waitForRequest(); } - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); } TEST(NetworkRequest, FinallyCallbackOnError) @@ -165,7 +165,7 @@ TEST(NetworkRequest, FinallyCallbackOnError) 411, 412, 413, 414, 418, 500, 501, 502, 503, 504, }; - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); for (const auto code : codes) { @@ -189,7 +189,7 @@ TEST(NetworkRequest, FinallyCallbackOnError) TEST(NetworkRequest, TimeoutTimingOut) { - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); auto url = getDelayURL(5); RequestWaiter waiter; @@ -214,12 +214,12 @@ TEST(NetworkRequest, TimeoutTimingOut) waiter.waitForRequest(); - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); } TEST(NetworkRequest, TimeoutNotTimingOut) { - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); auto url = getDelayURL(1); RequestWaiter waiter; @@ -240,12 +240,12 @@ TEST(NetworkRequest, TimeoutNotTimingOut) waiter.waitForRequest(); - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); } TEST(NetworkRequest, FinallyCallbackOnTimeout) { - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); auto url = getDelayURL(5); @@ -276,5 +276,5 @@ TEST(NetworkRequest, FinallyCallbackOnTimeout) EXPECT_TRUE(finallyCalled); EXPECT_TRUE(onErrorCalled); EXPECT_FALSE(onSuccessCalled); - EXPECT_TRUE(NetworkManager::workerThread.isRunning()); + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); } From ed20e71db4c957d3b2a8ce9350b847f4c805cb83 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 23 Mar 2024 12:22:42 +0100 Subject: [PATCH 11/33] refactor: adapt magic_enum to Qt (#5258) --- CHANGELOG.md | 1 + src/common/ChatterinoSetting.hpp | 12 +- src/common/network/NetworkPrivate.cpp | 7 +- src/common/network/NetworkPrivate.hpp | 2 +- .../commands/builtin/twitch/Announce.cpp | 6 +- src/controllers/plugins/LuaAPI.cpp | 9 +- src/controllers/plugins/Plugin.cpp | 23 +- src/controllers/plugins/Plugin.hpp | 14 +- src/controllers/plugins/PluginPermission.cpp | 9 +- src/providers/seventv/SeventvEventAPI.cpp | 3 +- src/providers/seventv/eventapi/Dispatch.cpp | 12 +- .../seventv/eventapi/Subscription.cpp | 16 +- src/providers/twitch/api/Helix.cpp | 5 +- .../twitch/pubsubmessages/AutoMod.cpp | 4 +- src/providers/twitch/pubsubmessages/Base.cpp | 4 +- .../twitch/pubsubmessages/ChannelPoints.cpp | 4 +- .../pubsubmessages/ChatModeratorAction.cpp | 4 +- .../twitch/pubsubmessages/LowTrustUsers.cpp | 17 +- .../twitch/pubsubmessages/Whisper.cpp | 4 +- src/util/QMagicEnum.hpp | 313 ++++++++++++++++++ src/widgets/helper/ChannelView.cpp | 4 +- src/widgets/settingspages/GeneralPage.cpp | 4 +- src/widgets/settingspages/GeneralPageView.hpp | 7 +- tests/CMakeLists.txt | 1 + tests/src/QMagicEnum.cpp | 198 +++++++++++ 25 files changed, 603 insertions(+), 80 deletions(-) create mode 100644 src/util/QMagicEnum.hpp create mode 100644 tests/src/QMagicEnum.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9496106cc28..865a4148121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,7 @@ - Dev: Cleaned up and optimized resources. (#5222) - Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) +- Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) ## 2.4.6 diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index be3ebb8ff4a..fe7e5ed65dc 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include "util/QMagicEnum.hpp" + #include #include @@ -108,10 +109,7 @@ class EnumStringSetting : public pajlada::Settings::Setting template EnumStringSetting &operator=(Enum newValue) { - std::string enumName(magic_enum::enum_name(newValue)); - auto qEnumName = QString::fromStdString(enumName); - - this->setValue(qEnumName.toLower()); + this->setValue(qmagicenum::enumNameString(newValue).toLower()); return *this; } @@ -130,8 +128,8 @@ class EnumStringSetting : public pajlada::Settings::Setting Enum getEnum() { - return magic_enum::enum_cast(this->getValue().toStdString(), - magic_enum::case_insensitive) + return qmagicenum::enumCast(this->getValue(), + qmagicenum::CASE_INSENSITIVE) .value_or(this->defaultValue); } diff --git a/src/common/network/NetworkPrivate.cpp b/src/common/network/NetworkPrivate.cpp index 51842dd578c..ed81dd9e6b6 100644 --- a/src/common/network/NetworkPrivate.cpp +++ b/src/common/network/NetworkPrivate.cpp @@ -9,6 +9,7 @@ #include "util/AbandonObject.hpp" #include "util/DebugCount.hpp" #include "util/PostToThread.hpp" +#include "util/QMagicEnum.hpp" #include #include @@ -181,11 +182,9 @@ void NetworkData::emitFinally() }); } -QLatin1String NetworkData::typeString() const +QString NetworkData::typeString() const { - auto view = magic_enum::enum_name(this->requestType); - return QLatin1String{view.data(), - static_cast(view.size())}; + return qmagicenum::enumNameString(this->requestType); } void load(std::shared_ptr &&data) diff --git a/src/common/network/NetworkPrivate.hpp b/src/common/network/NetworkPrivate.hpp index 1e169a927f4..434d9f66dd9 100644 --- a/src/common/network/NetworkPrivate.hpp +++ b/src/common/network/NetworkPrivate.hpp @@ -60,7 +60,7 @@ class NetworkData void emitError(NetworkResult &&result); void emitFinally(); - QLatin1String typeString() const; + QString typeString() const; private: QString hash_; diff --git a/src/controllers/commands/builtin/twitch/Announce.cpp b/src/controllers/commands/builtin/twitch/Announce.cpp index d14d1f914b3..a8674619564 100644 --- a/src/controllers/commands/builtin/twitch/Announce.cpp +++ b/src/controllers/commands/builtin/twitch/Announce.cpp @@ -30,11 +30,7 @@ QString sendAnnouncementColor(const CommandContext &ctx, QString colorStr = ""; if (color != HelixAnnouncementColor::Primary) { - colorStr = - QString::fromStdString( - std::string{ - magic_enum::enum_name(color)}) - .toLower(); + colorStr = qmagicenum::enumNameString(color).toLower(); } if (ctx.words.size() < 2) diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index 497c25260ea..d70be648943 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -119,9 +119,12 @@ int c2_register_callback(lua_State *L) return 0; } - auto callbackSavedName = QString("c2cb-%1").arg( - magic_enum::enum_name(evtType).data()); - lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str()); + auto typeName = magic_enum::enum_name(evtType); + std::string callbackSavedName; + callbackSavedName.reserve(5 + typeName.size()); + callbackSavedName += "c2cb-"; + callbackSavedName += typeName; + lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str()); lua_pop(L, 2); diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index fc0a255cc08..4609fee7c17 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -3,6 +3,7 @@ # include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" +# include "util/QMagicEnum.hpp" extern "C" { # include @@ -26,7 +27,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else if (!homepageObj.isUndefined()) { - QString type = magic_enum::enum_name(homepageObj.type()).data(); + auto type = qmagicenum::enumName(homepageObj.type()); this->errors.emplace_back( QString("homepage is defined but is not a string (its type is %1)") .arg(type)); @@ -38,7 +39,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(nameObj.type()).data(); + auto type = qmagicenum::enumName(nameObj.type()); this->errors.emplace_back( QString("name is not a string (its type is %1)").arg(type)); } @@ -50,7 +51,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(descrObj.type()).data(); + auto type = qmagicenum::enumName(descrObj.type()); this->errors.emplace_back( QString("description is not a string (its type is %1)").arg(type)); } @@ -64,7 +65,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = authorsArr.at(i); if (!t.isString()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back( QString("authors element #%1 is not a string (it is a %2)") .arg(i) @@ -76,7 +77,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(authorsObj.type()).data(); + auto type = qmagicenum::enumName(authorsObj.type()); this->errors.emplace_back( QString("authors is not an array (its type is %1)").arg(type)); } @@ -88,7 +89,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(licenseObj.type()).data(); + auto type = qmagicenum::enumName(licenseObj.type()); this->errors.emplace_back( QString("license is not a string (its type is %1)").arg(type)); } @@ -109,7 +110,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) } else { - QString type = magic_enum::enum_name(verObj.type()).data(); + auto type = qmagicenum::enumName(verObj.type()); this->errors.emplace_back( QString("version is not a string (its type is %1)").arg(type)); this->version = semver::version(0, 0, 0); @@ -119,7 +120,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) { if (!permsObj.isArray()) { - QString type = magic_enum::enum_name(permsObj.type()).data(); + auto type = qmagicenum::enumName(permsObj.type()); this->errors.emplace_back( QString("permissions is not an array (its type is %1)") .arg(type)); @@ -132,7 +133,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = permsArr.at(i); if (!t.isObject()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back(QString("permissions element #%1 is not " "an object (its type is %2)") .arg(i) @@ -161,7 +162,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) { if (!tagsObj.isArray()) { - QString type = magic_enum::enum_name(tagsObj.type()).data(); + auto type = qmagicenum::enumName(tagsObj.type()); this->errors.emplace_back( QString("tags is not an array (its type is %1)").arg(type)); return; @@ -173,7 +174,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj) const auto &t = tagsArr.at(i); if (!t.isString()) { - QString type = magic_enum::enum_name(t.type()).data(); + auto type = qmagicenum::enumName(t.type()); this->errors.push_back( QString("tags element #%1 is not a string (its type is %2)") .arg(i) diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 6dfb3b20e93..4450b2a0192 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -107,14 +107,14 @@ class Plugin return {}; } // this uses magic enum to help automatic tooling find usages + auto typeName = + magic_enum::enum_name(lua::api::EventType::CompletionRequested); + std::string cbName; + cbName.reserve(5 + typeName.size()); + cbName += "c2cb-"; + cbName += typeName; auto typ = - lua_getfield(this->state_, LUA_REGISTRYINDEX, - QString("c2cb-%1") - .arg(magic_enum::enum_name( - lua::api::EventType::CompletionRequested) - .data()) - .toStdString() - .c_str()); + lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str()); if (typ != LUA_TFUNCTION) { lua_pop(this->state_, 1); diff --git a/src/controllers/plugins/PluginPermission.cpp b/src/controllers/plugins/PluginPermission.cpp index d806db4bd30..09204f93df4 100644 --- a/src/controllers/plugins/PluginPermission.cpp +++ b/src/controllers/plugins/PluginPermission.cpp @@ -1,6 +1,8 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/PluginPermission.hpp" +# include "util/QMagicEnum.hpp" + # include # include @@ -11,14 +13,13 @@ PluginPermission::PluginPermission(const QJsonObject &obj) auto jsontype = obj.value("type"); if (!jsontype.isString()) { - QString tn = magic_enum::enum_name(jsontype.type()).data(); + auto tn = qmagicenum::enumName(jsontype.type()); this->errors.emplace_back(QString("permission type is defined but is " "not a string (its type is %1)") .arg(tn)); } - auto strtype = jsontype.toString().toStdString(); - auto opt = magic_enum::enum_cast( - strtype, magic_enum::case_insensitive); + auto opt = qmagicenum::enumCast( + jsontype.toString(), qmagicenum::CASE_INSENSITIVE); if (!opt.has_value()) { this->errors.emplace_back(QString("permission type is an unknown (%1)") diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 2b8c0ec27d1..82234a99c9f 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -6,6 +6,7 @@ #include "providers/seventv/eventapi/Message.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvCosmetics.hpp" +#include "util/QMagicEnum.hpp" #include @@ -228,7 +229,7 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) default: { qCDebug(chatterinoSeventvEventAPI) << "Unknown subscription type:" - << magic_enum::enum_name(dispatch.type).data() + << qmagicenum::enumName(dispatch.type) << "body:" << dispatch.body; } break; diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp index 03fbdac970c..b4fd3104400 100644 --- a/src/providers/seventv/eventapi/Dispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -1,5 +1,7 @@ #include "providers/seventv/eventapi/Dispatch.hpp" +#include "util/QMagicEnum.hpp" + #include #include @@ -7,8 +9,7 @@ namespace chatterino::seventv::eventapi { Dispatch::Dispatch(QJsonObject obj) - : type(magic_enum::enum_cast( - obj["type"].toString().toStdString()) + : type(qmagicenum::enumCast(obj["type"].toString()) .value_or(SubscriptionType::INVALID)) , body(obj["body"].toObject()) , id(this->body["id"].toString()) @@ -95,8 +96,8 @@ bool UserConnectionUpdateDispatch::validate() const CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch) : data(dispatch.body["object"]["data"].toObject()) - , kind(magic_enum::enum_cast( - dispatch.body["object"]["kind"].toString().toStdString()) + , kind(qmagicenum::enumCast( + dispatch.body["object"]["kind"].toString()) .value_or(CosmeticKind::INVALID)) { } @@ -111,8 +112,7 @@ EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch( { const auto obj = dispatch.body["object"].toObject(); this->refID = obj["ref_id"].toString(); - this->kind = magic_enum::enum_cast( - obj["kind"].toString().toStdString()) + this->kind = qmagicenum::enumCast(obj["kind"].toString()) .value_or(CosmeticKind::INVALID); const auto userConnections = obj["user"]["connections"].toArray(); diff --git a/src/providers/seventv/eventapi/Subscription.cpp b/src/providers/seventv/eventapi/Subscription.cpp index 91d330c5e61..2a1a46a94a7 100644 --- a/src/providers/seventv/eventapi/Subscription.cpp +++ b/src/providers/seventv/eventapi/Subscription.cpp @@ -1,5 +1,7 @@ #include "providers/seventv/eventapi/Subscription.hpp" +#include "util/QMagicEnum.hpp" + #include #include #include @@ -9,14 +11,15 @@ namespace { +using namespace chatterino; using namespace chatterino::seventv::eventapi; -const char *typeToString(SubscriptionType type) +QString typeToString(SubscriptionType type) { - return magic_enum::enum_name(type).data(); + return qmagicenum::enumNameString(type); } -QJsonObject createDataJson(const char *typeName, const Condition &condition) +QJsonObject createDataJson(const QString &typeName, const Condition &condition) { QJsonObject data; data["type"] = typeName; @@ -45,7 +48,7 @@ bool Subscription::operator!=(const Subscription &rhs) const QByteArray Subscription::encodeSubscribe() const { - const auto *typeName = typeToString(this->type); + auto typeName = typeToString(this->type); QJsonObject root; root["op"] = (int)Opcode::Subscribe; root["d"] = createDataJson(typeName, this->condition); @@ -54,7 +57,7 @@ QByteArray Subscription::encodeSubscribe() const QByteArray Subscription::encodeUnsubscribe() const { - const auto *typeName = typeToString(this->type); + auto typeName = typeToString(this->type); QJsonObject root; root["op"] = (int)Opcode::Unsubscribe; root["d"] = createDataJson(typeName, this->condition); @@ -66,8 +69,7 @@ QDebug &operator<<(QDebug &dbg, const Subscription &subscription) std::visit( [&](const auto &cond) { dbg << "Subscription{ condition:" << cond - << "type:" << magic_enum::enum_name(subscription.type).data() - << '}'; + << "type:" << qmagicenum::enumName(subscription.type) << '}'; }, subscription.condition); return dbg; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 2a3b9a14e10..daf17021f0c 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -5,6 +5,7 @@ #include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include "util/CancellationToken.hpp" +#include "util/QMagicEnum.hpp" #include #include @@ -1172,9 +1173,7 @@ void Helix::sendChatAnnouncement( QJsonObject body; body.insert("message", message); - const auto colorStr = - std::string{magic_enum::enum_name(color)}; - body.insert("color", QString::fromStdString(colorStr).toLower()); + body.insert("color", qmagicenum::enumNameString(color).toLower()); this->makePost("chat/announcements", urlQuery) .json(body) diff --git a/src/providers/twitch/pubsubmessages/AutoMod.cpp b/src/providers/twitch/pubsubmessages/AutoMod.cpp index 8c0838f6b57..697db1e32c7 100644 --- a/src/providers/twitch/pubsubmessages/AutoMod.cpp +++ b/src/providers/twitch/pubsubmessages/AutoMod.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/AutoMod.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) @@ -7,7 +9,7 @@ PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) , data(root.value("data").toObject()) , status(this->data.value("status").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/Base.cpp b/src/providers/twitch/pubsubmessages/Base.cpp index 7bc4a2f5f79..4b32786e969 100644 --- a/src/providers/twitch/pubsubmessages/Base.cpp +++ b/src/providers/twitch/pubsubmessages/Base.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/Base.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubMessage::PubSubMessage(QJsonObject _object) @@ -9,7 +11,7 @@ PubSubMessage::PubSubMessage(QJsonObject _object) , error(this->object.value("error").toString()) , typeString(this->object.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp index 8907a2d2ea2..244e2be98c0 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChannelPoints.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( @@ -7,7 +9,7 @@ PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp index 8134178c5d1..2cc36ca9830 100644 --- a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp +++ b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( @@ -7,7 +9,7 @@ PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp index 2a7fd6f50fc..cac4e02fd24 100644 --- a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" +#include "util/QMagicEnum.hpp" + #include #include @@ -8,8 +10,7 @@ namespace chatterino { PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) : typeString(root.value("type").toString()) { - if (const auto oType = - magic_enum::enum_cast(this->typeString.toStdString()); + if (const auto oType = qmagicenum::enumCast(this->typeString); oType.has_value()) { this->type = oType.value(); @@ -75,8 +76,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) this->updatedByUserDisplayName = updatedBy.value("display_name").toString(); this->treatmentString = data.value("treatment").toString(); - if (const auto oTreatment = magic_enum::enum_cast( - this->treatmentString.toStdString()); + if (const auto oTreatment = + qmagicenum::enumCast(this->treatmentString); oTreatment.has_value()) { this->treatment = oTreatment.value(); @@ -84,8 +85,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) this->evasionEvaluationString = data.value("ban_evasion_evaluation").toString(); - if (const auto oEvaluation = magic_enum::enum_cast( - this->evasionEvaluationString.toStdString()); + if (const auto oEvaluation = qmagicenum::enumCast( + this->evasionEvaluationString); oEvaluation.has_value()) { this->evasionEvaluation = oEvaluation.value(); @@ -93,8 +94,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) for (const auto &rType : data.value("types").toArray()) { - if (const auto oRestriction = magic_enum::enum_cast( - rType.toString().toStdString()); + if (const auto oRestriction = + qmagicenum::enumCast(rType.toString()); oRestriction.has_value()) { this->restrictionTypes.set(oRestriction.value()); diff --git a/src/providers/twitch/pubsubmessages/Whisper.cpp b/src/providers/twitch/pubsubmessages/Whisper.cpp index d0b59d0c6ae..2001b8ccbdd 100644 --- a/src/providers/twitch/pubsubmessages/Whisper.cpp +++ b/src/providers/twitch/pubsubmessages/Whisper.cpp @@ -1,11 +1,13 @@ #include "providers/twitch/pubsubmessages/Whisper.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubWhisperMessage::PubSubWhisperMessage(const QJsonObject &root) : typeString(root.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/util/QMagicEnum.hpp b/src/util/QMagicEnum.hpp new file mode 100644 index 00000000000..0325102e3b8 --- /dev/null +++ b/src/util/QMagicEnum.hpp @@ -0,0 +1,313 @@ +#pragma once + +#include +#include +#include + +namespace chatterino::qmagicenum::detail { + +template +struct EnableIfEnum { +}; + +template +struct EnableIfEnum { + using type = R; +}; + +template , + typename D = std::decay_t> +using enable_if_t = typename EnableIfEnum< + std::is_enum_v && + std::is_invocable_r_v, + R>::type; + +template +consteval QStringView fromArray(const std::array &arr) +{ + return QStringView{arr.data(), static_cast(N - 1)}; +} + +// Only the latin1 subset may be used right now, since it's easily convertible +template +consteval bool isLatin1(std::string_view maybe) +{ + for (std::size_t i = 0; i < N; i++) + { + if (maybe[i] < 0x20 || maybe[i] > 0x7e) + { + return false; + } + } + return true; +} + +template +inline constexpr bool eq( + QStringView a, QStringView b, + [[maybe_unused]] BinaryPredicate && + p) noexcept(magic_enum::detail::is_nothrow_invocable()) +{ + // Note: operator== isn't constexpr + if (a.size() != b.size()) + { + return false; + } + + for (QStringView::size_type i = 0; i < a.size(); i++) + { + if (!p(a[i], b[i])) + { + return false; + } + } + + return true; +} + +template +consteval auto enumNameStorage() +{ + constexpr auto utf8 = magic_enum::enum_name(); + + static_assert(isLatin1(utf8), + "Can't convert non-latin1 UTF8 to UTF16"); + + std::array storage; + for (std::size_t i = 0; i < utf8.size(); i++) + { + storage[i] = static_cast(utf8[i]); + } + storage[utf8.size()] = 0; + return storage; +} + +template +inline constexpr auto ENUM_NAME_STORAGE = enumNameStorage(); + +template +consteval auto namesStorage(std::index_sequence /*unused*/) +{ + return std::array{{detail::fromArray( + ENUM_NAME_STORAGE()[I]>)...}}; +} + +template > +inline constexpr auto NAMES_STORAGE = namesStorage( + std::make_index_sequence()>{}); + +template > +using NamesStorage = decltype((NAMES_STORAGE)); + +template > +class CaseInsensitive +{ + static constexpr QChar toLower(QChar c) noexcept + { + return (c >= u'A' && c <= u'Z') + ? QChar(c.unicode() + static_cast(u'a' - u'A')) + : c; + } + +public: + template + constexpr std::enable_if_t, QChar> && + std::is_same_v, QChar>, + bool> + operator()(L lhs, R rhs) const noexcept + { + return Op{}(toLower(lhs), toLower(rhs)); + } +}; + +} // namespace chatterino::qmagicenum::detail + +namespace chatterino::qmagicenum { + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string view +template +[[nodiscard]] consteval detail::enable_if_t + enumName() noexcept +{ + return QStringView{ + detail::fromArray(detail::ENUM_NAME_STORAGE)}; +} + +/// @brief Get the name of an enum value +/// +/// @param value The enum value +/// @returns The name as a string view. If @a value does not have name or the +/// value is out of range an empty string is returned. +template > +[[nodiscard]] constexpr detail::enable_if_t enumName( + E value) noexcept +{ + using D = std::decay_t; + + if (const auto i = magic_enum::enum_index(value)) + { + return detail::NAMES_STORAGE[*i]; + } + return {}; +} + +/// @brief Gets a static QString from @a view. +/// +/// @pre @a view must be a static string view (i.e. it must be valid throughout +/// the entire duration of the program). +/// +/// @param view The view to turn into a static string +/// @returns Qt6: A static string (never gets freed), Qt5: regular string +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +[[nodiscard]] inline QString staticString(QStringView view) noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return QString(QStringPrivate(nullptr, const_cast(view.utf16()), + view.size())); +} +#else +[[nodiscard]] inline QString staticString(QStringView view) +{ + return view.toString(); +} +#endif + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. The returned string is static. +template +[[nodiscard]] inline detail::enable_if_t + enumNameString() noexcept +{ + return staticString(enumName()); +} + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. If @a value does not have name or the +/// value is out of range an empty string is returned. +/// The returned string is static. +template > +[[nodiscard]] inline detail::enable_if_t enumNameString( + E value) noexcept +{ + using D = std::decay_t; + + return staticString(enumName(value)); +} + +/// @brief Gets the enum value from a name +/// +/// @tparam E The enum type to parse the @a name as +/// @param name The name of the enum value to parse +/// @param p A predicate to compare characters of a string +/// (defaults to std::equal_to) +/// @returns A `std::optional` of the parsed value. If no value was parsed, +/// `std::nullopt` is returned. +template , + typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr detail::enable_if_t>, + BinaryPredicate> + enumCast(QStringView name, + [[maybe_unused]] BinaryPredicate p = + {}) noexcept(magic_enum::detail:: + is_nothrow_invocable()) +{ + using D = std::decay_t; + + if constexpr (magic_enum::enum_count() == 0) + { + static_cast(name); + return std::nullopt; // Empty enum. + } + + for (std::size_t i = 0; i < magic_enum::enum_count(); i++) + { + if (detail::eq(name, detail::NAMES_STORAGE[i], p)) + { + return magic_enum::enum_value(i); + } + } + return std::nullopt; // Invalid value or out of range. +} + +/// @brief Constructs a name from the @a flags +/// +/// @param flags The combined flags to construct the name from +/// @param sep A separator between each flag (defaults to u'|') +/// @returns A string containing all names separated by @a sep. If any flag in +/// @a flags is out of rage or does not have a name, an empty string +/// is returned. +template +[[nodiscard]] inline detail::enable_if_t enumFlagsName( + E flags, char16_t sep = u'|') +{ + using D = std::decay_t; + using U = std::underlying_type_t; + constexpr auto S = magic_enum::detail::enum_subtype::flags; // NOLINT + + QString name; + auto checkValue = U{0}; + for (std::size_t i = 0; i < magic_enum::enum_count(); ++i) + { + const auto v = static_cast(magic_enum::enum_value(i)); + if ((static_cast(flags) & v) != 0) + { + const auto n = detail::NAMES_STORAGE[i]; + if (!n.empty()) + { + checkValue |= v; + if (!name.isEmpty()) + { + name.append(sep); + } + name.append(n); + } + else + { + return {}; // Value out of range. + } + } + } + + if (checkValue != 0 && checkValue == static_cast(flags)) + { + return name; + } + return {}; // Invalid value or out of range. +} + +/// @brief Get the names of all values from @a E. +/// +/// @tparam E The enum type +/// @returns A `std::array` of all names (`QStringView`s) +template > +[[nodiscard]] constexpr auto enumNames() noexcept + -> detail::enable_if_t> +{ + return detail::NAMES_STORAGE, S>; +} + +/// Allows you to write qmagicenum::enumCast("bar", qmagicenum::CASE_INSENSITIVE) +inline constexpr auto CASE_INSENSITIVE = detail::CaseInsensitive<>{}; + +} // namespace chatterino::qmagicenum diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3cf64e1160c..1454b4999de 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -33,6 +33,7 @@ #include "util/DistanceBetweenPoints.hpp" #include "util/Helpers.hpp" #include "util/IncognitoBrowser.hpp" +#include "util/QMagicEnum.hpp" #include "util/Twitch.hpp" #include "widgets/dialogs/ReplyThreadPopup.hpp" #include "widgets/dialogs/SettingsDialog.hpp" @@ -266,8 +267,7 @@ void addHiddenContextMenuItems(QMenu *menu, jsonObject["id"] = message->id; jsonObject["searchText"] = message->searchText; jsonObject["messageText"] = message->messageText; - jsonObject["flags"] = QString::fromStdString( - magic_enum::enum_flags_name(message->flags.value())); + jsonObject["flags"] = qmagicenum::enumFlagsName(message->flags.value()); jsonDocument.setObject(jsonObject); diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 50136dceef2..74746be8ad1 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1243,7 +1243,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) helixTimegateModerators->minimumSizeHint().width()); layout.addDropdownEnumClass( - "Chat send protocol", magic_enum::enum_names(), + "Chat send protocol", qmagicenum::enumNames(), s.chatSendProtocol, "'Helix' will use Twitch's Helix API to send message. 'IRC' will use " "IRC to send messages.", @@ -1256,7 +1256,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) auto *soundBackend = layout.addDropdownEnumClass( "Sound backend (requires restart)", - magic_enum::enum_names(), s.soundBackend, + qmagicenum::enumNames(), s.soundBackend, "Change this only if you're noticing issues with sound playback on " "your system", {}); diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index d2a0a27e121..7e1625a7084 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -276,7 +276,7 @@ class GeneralPageView : public QWidget template ComboBox *addDropdownEnumClass(const QString &text, - const std::array &items, + const std::array &items, EnumStringSetting &setting, QString toolTipText, const QString &defaultValueText) @@ -285,7 +285,7 @@ class GeneralPageView : public QWidget for (const auto &item : items) { - combo->addItem(QString::fromStdString(std::string(item))); + combo->addItem(item.toString()); } if (!defaultValueText.isEmpty()) @@ -296,8 +296,7 @@ class GeneralPageView : public QWidget setting.connect( [&setting, combo](const QString &value) { auto enumValue = - magic_enum::enum_cast(value.toStdString(), - magic_enum::case_insensitive) + qmagicenum::enumCast(value, qmagicenum::CASE_INSENSITIVE) .value_or(setting.defaultValue); auto i = magic_enum::enum_integer(enumValue); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f1f80cf0e7b..fb5730048b8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp # Add your new file above this line! ) diff --git a/tests/src/QMagicEnum.cpp b/tests/src/QMagicEnum.cpp new file mode 100644 index 00000000000..80c265efe24 --- /dev/null +++ b/tests/src/QMagicEnum.cpp @@ -0,0 +1,198 @@ +#include "util/QMagicEnum.hpp" + +#include "common/FlagsEnum.hpp" +#include "common/Literals.hpp" + +#include + +using namespace chatterino; +using namespace literals; + +using qmagicenum::enumCast; +using qmagicenum::enumFlagsName; +using qmagicenum::enumName; +using qmagicenum::enumNames; +using qmagicenum::enumNameString; + +namespace { + +enum class MyEnum { + Foo, + Bar, + Baz, +}; + +enum class MyFlag { + None = 0, + One = 1, + Two = 2, + Four = 4, + Eight = 8, +}; +using MyFlags = chatterino::FlagsEnum; + +enum class MyCustom { + Default = 1, + First = 4, + Second = 9, +}; + +enum MyOpen { + OpenOne = 11, + OpenTwo = 12, + OpenThree = 13, +}; + +consteval bool eq(QStringView a, QStringView b) +{ + return qmagicenum::detail::eq(a, b, std::equal_to<>()); +} + +template +consteval bool checkConst(E value, QStringView expectedName) +{ + return eq(enumName(value), expectedName) && + enumCast(expectedName) == value; +} + +template +consteval bool checkInsensitive(E value, QStringView possible) +{ + return enumCast(possible, qmagicenum::CASE_INSENSITIVE) == value; +} + +template ().size()> +consteval bool checkValues(std::array values) +{ + constexpr auto got = enumNames(); + if (got.size() != N) + { + return false; + } + for (size_t i = 0; i < N; i++) + { + if (!eq(got.at(i), values.at(i))) + { + return false; + } + } + return true; +} + +} // namespace + +template <> +struct magic_enum::customize::enum_range { + static constexpr bool is_flags = true; // NOLINT +}; + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name(MyCustom value) noexcept +{ + switch (value) + { + case MyCustom::First: + return "myfirst"; + case MyCustom::Second: + return "mysecond.*"; + + default: + return default_tag; + } +} + +TEST(QMagicEnum, basic) +{ + static_assert(eq(enumName(), u"Foo")); + static_assert(eq(enumName(), u"Bar")); + static_assert(eq(enumName(), u"Baz")); + static_assert(checkConst(MyEnum::Foo, u"Foo")); + static_assert(checkConst(MyEnum::Bar, u"Bar")); + static_assert(checkConst(MyEnum::Baz, u"Baz")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"Foo", u"Bar", u"Baz"})); +} + +TEST(QMagicEnum, flags) +{ + static_assert(eq(enumName(), u"None")); + static_assert(eq(enumName(), u"One")); + static_assert(eq(enumName(), u"Two")); + static_assert(eq(enumName(), u"Four")); + static_assert(eq(enumName(), u"Eight")); + + static_assert(!magic_enum::enum_index(MyFlag::None).has_value()); + static_assert(eq(enumName(MyFlag::None), u"")); + + static_assert(checkConst(MyFlag::One, u"One")); + static_assert(checkConst(MyFlag::Two, u"Two")); + static_assert(checkConst(MyFlag::Four, u"Four")); + static_assert(checkConst(MyFlag::Eight, u"Eight")); + static_assert(checkConst(MyFlag::Eight, u"Eight")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"One", u"Two", u"Four", u"Eight"})); +} + +TEST(QMagicEnum, enumNameString) +{ + ASSERT_EQ(enumNameString(), u"Baz"); + + ASSERT_EQ(enumNameString(), u"None"); + ASSERT_EQ(enumNameString(), u"Four"); + + ASSERT_EQ(enumNameString(MyEnum::Bar), u"Bar"); + ASSERT_EQ(enumNameString(MyFlag::None), u""); + ASSERT_EQ(enumNameString(MyFlag::One), u"One"); + ASSERT_EQ(enumNameString(MyCustom::Second), u"mysecond.*"); + ASSERT_EQ(enumNameString(OpenTwo), u"OpenTwo"); +} + +TEST(QMagicEnum, enumFlagsName) +{ + ASSERT_EQ(enumFlagsName(MyFlag::Eight), u"Eight"_s); + ASSERT_EQ(enumFlagsName(MyFlag::None), u""_s); + ASSERT_EQ(enumFlagsName(MyFlags{MyFlag::Eight, MyFlag::Four}.value(), u'+'), + u"Four+Eight"_s); + ASSERT_EQ(enumFlagsName( + MyFlags{MyFlag::Eight, MyFlag::One, MyFlag::Two, MyFlag::Four} + .value()), + u"One|Two|Four|Eight"_s); + ASSERT_EQ( + enumFlagsName(MyFlags{MyFlag::One, static_cast(16)}.value()), + u""_s); +} + +TEST(QMagicEnum, renamed) +{ + static_assert(eq(enumName(), u"Default")); + static_assert(eq(enumName(), u"myfirst")); + static_assert(eq(enumName(), u"mysecond.*")); + static_assert(checkConst(MyCustom::Default, u"Default")); + static_assert(checkConst(MyCustom::First, u"myfirst")); + static_assert(checkConst(MyCustom::Second, u"mysecond.*")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert( + checkValues({u"Default", u"myfirst", u"mysecond.*"})); +} + +TEST(QMagicEnum, open) +{ + static_assert(eq(enumName(), u"OpenOne")); + static_assert(eq(enumName(), u"OpenTwo")); + static_assert(eq(enumName(), u"OpenThree")); + static_assert(checkConst(OpenOne, u"OpenOne")); + static_assert(checkConst(OpenTwo, u"OpenTwo")); + static_assert(checkConst(OpenThree, u"OpenThree")); + static_assert(eq(enumName(static_cast(16)), u"")); + static_assert(checkValues({u"OpenOne", u"OpenTwo", u"OpenThree"})); +} + +TEST(QMagicEnum, caseInsensitive) +{ + static_assert(checkInsensitive(MyEnum::Foo, u"foo")); + static_assert(checkInsensitive(MyEnum::Bar, u"BAR")); + static_assert(checkInsensitive(MyFlag::Four, u"fOUR")); + static_assert(checkInsensitive(MyCustom::Second, u"MySecond.*")); + static_assert(checkInsensitive(OpenOne, u"openone")); +} From ca69172479d751ec67632f35653d1b1aacd58753 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Sun, 24 Mar 2024 03:30:22 -0700 Subject: [PATCH 12/33] fix: check broadcast binaries without case sensitivity (#5260) --- CHANGELOG.md | 1 + src/singletons/StreamerMode.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 865a4148121..b74cadd2656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) +- Bugfix: Detect when OBS is running on MacOS. (#5260) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index cb73112757f..7ee4fa5884b 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -52,7 +52,7 @@ bool isBroadcasterSoftwareActive() { #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) QProcess p; - p.start("pgrep", {"-x", broadcastingBinaries().join("|")}, + p.start("pgrep", {"-xi", broadcastingBinaries().join("|")}, QIODevice::NotOpen); if (p.waitForFinished(1000) && p.exitStatus() == QProcess::NormalExit) From 00119a8e3e79950546eb8b1fea225ac63b80a743 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 24 Mar 2024 12:48:00 +0100 Subject: [PATCH 13/33] fix: Update Linux build instructions (#5262) --- BUILDING_ON_LINUX.md | 46 +++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 67ae8fe79d7..b901e8e6d7e 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -1,38 +1,49 @@ # Linux -Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.12 or newer**. +For all dependencies below we use Qt6. Our minimum supported version is Qt5.15, but you are on your own. ## Install dependencies -### Ubuntu 20.04 +### Ubuntu -_Most likely works the same for other Debian-like distros._ +Building on Ubuntu requires Docker. -Install all the dependencies using `sudo apt install qttools5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++ libsecret-1-dev` +Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-20.04 as your base if you're on Ubuntu 20.04. + +Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-22.04 if you're on Ubuntu 22.04. + +The built binary should be exportable from the final image & able to run on your system assuming you perform a static build. See our [build.yml github workflow file](.github/workflows/build.yml) for the cmake line used for Ubuntu builds. + +### Debian 12 (bookworm) or later + +```sh +sudo apt install qt6-base-dev qt6-5compat-dev qt6-svg-dev qt6-image-formats-plugins libboost1.81-dev libssl-dev cmake g++ git +``` ### Arch Linux -Install all the dependencies using `sudo pacman -S --needed qt5-base qt5-imageformats qt5-svg qt5-tools boost rapidjson pkgconf openssl cmake` +```sh +sudo pacman -S --needed qt6-base qt6-tools boost-libs openssl qt6-imageformats qt6-5compat qt6-svg boost rapidjson pkgconf openssl cmake +``` Alternatively you can use the [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) package to build and install Chatterino for you. -### Fedora 28 and above +### Fedora 39 and above _Most likely works the same for other Red Hat-like distros. Substitute `dnf` with `yum`._ -Install all the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtimageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake` +```sh +sudo dnf install qt6-qtbase-devel qt6-qtimageformats qt6-qtsvg-devel qt6-qt5compat-devel g++ git openssl-devel boost-devel cmake +``` ### NixOS 18.09+ -Enter the development environment with all the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake` +```sh +nix-shell -p openssl boost qt6.full pkg-config cmake +``` ## Compile -### Through Qt Creator - -1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` -1. Open `CMakeLists.txt` with Qt Creator and select build - ## Manually 1. In the project directory, create a build directory and enter it @@ -42,9 +53,14 @@ Enter the development environment with all the dependencies: `nix-shell -p opens ``` 1. Generate build files ```sh - cmake .. + cmake -DBUILD_WITH_QT6=ON -DBUILD_WITH_QTKEYCHAIN=OFF .. ``` 1. Build the project ```sh - make + cmake --build . ``` + +### Through Qt Creator + +1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` (Or whatever equivalent for your distro) +1. Open `CMakeLists.txt` with Qt Creator and select build From 2750c528afd26bd54c6f99f1a64678afbe609648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Partyka?= <54620595+Heryin@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:10:40 +0100 Subject: [PATCH 14/33] fix: remove ":" from the message the user is replying to if it's a /me message (#5263) --- CHANGELOG.md | 1 + src/providers/twitch/TwitchMessageBuilder.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b74cadd2656..1c039db91a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) - Bugfix: Detect when OBS is running on MacOS. (#5260) +- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index af266889c0b..88c0f671e74 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -893,8 +893,10 @@ void TwitchMessageBuilder::parseThread() ->setLink({Link::ViewThread, this->thread_->rootId()}); this->emplace( - "@" + usernameText + ":", MessageElementFlag::RepliedMessage, - threadRoot->usernameColor, FontStyle::ChatMediumSmall) + "@" + usernameText + + (threadRoot->flags.has(MessageFlag::Action) ? "" : ":"), + MessageElementFlag::RepliedMessage, threadRoot->usernameColor, + FontStyle::ChatMediumSmall) ->setLink({Link::UserInfo, threadRoot->displayName}); MessageColor color = MessageColor::Text; From fb6beb4acabeb04464f27972f232468098883b65 Mon Sep 17 00:00:00 2001 From: DatGuy1 Date: Tue, 26 Mar 2024 20:51:16 +0200 Subject: [PATCH 15/33] fix: add trailing space if deletion link included in image upload response (#5269) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c039db91a1..5245db53b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) - Bugfix: Detect when OBS is running on MacOS. (#5260) +- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index af5dadcf707..cc71558f65e 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -543,7 +543,7 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/, // This also ensures that the LinkResolver doesn't get these links. addText(imageLink, MessageColor::Link) ->setLink({Link::Url, imageLink}) - ->setTrailingSpace(false); + ->setTrailingSpace(!deletionLink.isEmpty()); if (!deletionLink.isEmpty()) { From 337ae52a5d581b8b9d523f063ea9b8009a2b081e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:27:45 +0000 Subject: [PATCH 16/33] chore(deps): bump ZedThree/clang-tidy-review from 0.17.2 to 0.17.3 (#5271) Bumps [ZedThree/clang-tidy-review](https://github.com/zedthree/clang-tidy-review) from 0.17.2 to 0.17.3. - [Release notes](https://github.com/zedthree/clang-tidy-review/releases) - [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md) - [Commits](https://github.com/zedthree/clang-tidy-review/compare/v0.17.2...v0.17.3) --- updated-dependencies: - dependency-name: ZedThree/clang-tidy-review dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/clang-tidy.yml | 4 ++-- .github/workflows/post-clang-tidy-review.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 0f072a36442..b522bba5d3e 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -119,7 +119,7 @@ jobs: - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.2 + uses: ZedThree/clang-tidy-review@v0.17.3 with: build_dir: build-clang-tidy config_file: ".clang-tidy" @@ -145,4 +145,4 @@ jobs: libbenchmark-dev - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.2 + uses: ZedThree/clang-tidy-review/upload@v0.17.3 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index e22b264f502..2f9b6b3d9fa 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -14,6 +14,6 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.2 + - uses: ZedThree/clang-tidy-review/post@v0.17.3 with: lgtm_comment_body: "" From 515a92d6f7643c6af205e830e5964edfa98adffa Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:59:57 -0400 Subject: [PATCH 17/33] Prepare changelog for v2.5.0 release (#5264) --- CHANGELOG.md | 120 +++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5245db53b29..a8182382c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,119 +2,119 @@ ## Unversioned -- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922) -- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) -- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060) -- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) -- Minor: The account switcher is now styled to match your theme. (#4817) -- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) -- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847) -- Minor: Allow running `/ban`, `/timeout`, `/unban`, and `/untimeout` on User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957) -- Minor: The `/usercard` command now accepts user ids. (#4934) -- Minor: Add menu actions to reply directly to a message or the original thread root. (#4923) -- Minor: The `/reply` command now replies to the latest message of the user. (#4919) -- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) -- Minor: Add an option to use new experimental smarter emote completion. (#4987) -- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) -- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - right now only supporting bot badges for your chat bots. (#5119) -- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) -- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047) -- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) +- Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) +- Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) +- Major: Moderators can now see restricted chat messages and suspicious treatment updates. (#5056, #5060) +- Minor: Migrated to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) +- Minor: Moderation commands such as `/ban`, `/timeout`, `/unban`, and `/untimeout` can now be used via User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957) +- Minor: The `/usercard` command now accepts user ids. (`/usercard id:22484632`) (#4934) +- Minor: Added menu actions to reply directly to a message or the original thread root. (#4923) +- Minor: The `/reply` command now replies to the latest message from the user. Due to this change, the message you intended to reply to is now shown in the reply context, instead of the first message in a thread. (#4919) - Minor: The chatter list button is now hidden if you don't have moderator privileges. (#5245) +- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237) +- Minor: Allowed theming of tab live and rerun indicators. (#5188) +- Minor: The _Restart on crash_ setting works again on Windows. (#5012) +- Minor: Added an option to use new experimental smarter emote completion. (#4987) +- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - currently only supports bot badges for your chat bots. (#5119) +- Minor: Added support to send /announce[color] commands. Colored announcements only appear with the chosen color in Twitch chat. (#5250) - Minor: The whisper highlight color can now be configured through the settings. (#5053) - Minor: Added an option to always include the broadcaster in user completions. This is enabled by default. (#5193, #5244) -- Minor: Added missing periods at various moderator messages and commands. (#5061) -- Minor: Improved color selection and display. (#5057) -- Minor: Improved Streamlink documentation in the settings dialog. (#5076) -- Minor: Normalized the input padding between light & dark themes. (#5095) -- Minor: Add `--activate ` (or `-a`) command line option to activate or add a Twitch channel. (#5111) -- Minor: Chatters from recent-messages are now added to autocompletion. (#5116) -- Minor: Added a _System_ theme that updates according to the system's color scheme (requires Qt 6.5). (#5118) -- Minor: Added icons for newer versions of macOS. (#5148) -- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197) +- Minor: Added a warning message if you have multiple commands with the same trigger. (#4322) +- Minor: Chatters from message history are now added to autocompletion. (#5116) - Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130) -- Minor: Add a new Channel API for experimental plugins feature. (#5141, #5184, #5187) +- Minor: Added `--activate ` (or `-a`) command line option to focus or add a certain Twitch channel on startup. (#5111) +- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197) - Minor: Added the ability to change the top-most status of a window regardless of the _Always on top_ setting (right click the notebook). (#5135) -- Minor: Introduce `c2.later()` function to Lua API. (#5154) -- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237) -- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Added the ability to show AutoMod caught messages in mentions. (#5215) - Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215) -- Minor: Allow theming of tab live and rerun indicators. (#5188) +- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) +- Minor: Added icons for newer versions of macOS. (#5148) +- Minor: Improved color selection and display. (#5057) +- Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118) +- Minor: Normalized the input padding between light & dark themes. (#5095) +- Minor: The account switcher is now styled to match your theme. (#4817) - Minor: Added a fallback theme field to custom themes that will be used in case the custom theme does not contain a color Chatterino needs. If no fallback theme is specified, we'll pull the color from the included Dark or Light theme. (#5198) +- Minor: Added a new completion API for experimental plugins feature. (#5000, #5047) +- Minor: Added a new Channel API for experimental plugins feature. (#5141, #5184, #5187) +- Minor: Introduce `c2.later()` function to Lua API. (#5154) +- Minor: Added `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) +- Minor: Added wrappers for Lua `io` library for experimental plugins feature. (#5231) +- Minor: Added permissions to experimental plugins feature. (#5231) +- Minor: Added missing periods at various moderator messages and commands. (#5061) +- Minor: Improved Streamlink documentation in the settings dialog. (#5076) +- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847) +- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) +- Minor: Added an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) +- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) - Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) -- Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231) -- Minor: Add permissions to experimental plugins feature. (#5231) -- Minor: Added warning message if you have multiple commands with the same trigger. (#4322) -- Minor: Add support to send /announce[color] commands. (#5250) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) +- Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) - Bugfix: Fixed a performance issue when displaying replies to certain messages. (#4807) - Bugfix: Fixed an issue where certain parts of the split input wouldn't focus the split when clicked. (#4958) -- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) -- Bugfix: Fixed `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) -- Bugfix: Fixed Usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) +- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172)\ +- Bugfix: User text input within watch streak notices now correctly shows up. (#5029) - Bugfix: Fixed selection of tabs after closing a tab when using "Live Tabs Only". (#4770) -- Bugfix: Fixed input in reply thread popup losing focus when dragging. (#4815) -- Bugfix: Fixed the Quick Switcher (CTRL+K) from sometimes showing up on the wrong window. (#4819) +- Bugfix: Fixed input in the reply thread popup losing focus when dragging said window. (#4815) +- Bugfix: Fixed the Quick Switcher (CTRL+K) sometimes showing up on the wrong window. (#4819) - Bugfix: Fixed the font switcher not remembering what font you had previously selected. (#5224) - Bugfix: Fixed too much text being copied when copying chat messages. (#4812, #4830, #4839) -- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855) -- Bugfix: Fixed empty page being added when showing out of bounds dialog. (#4849) -- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained text input. (#5117) - Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873) +- Bugfix: Fixed an issue where Streamer Mode did not detect that OBS was running on MacOS. (#5260) +- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) +- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) +- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855) +- Bugfix: Fixed an empty page being added when showing the out of bounds dialog. (#4849) +- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained user text input. (#5117) - Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875, #4977, #5174) -- Bugfix: Fixed the input completion popup from disappearing when clicking on it on Windows and macOS. (#4876) +- Bugfix: Fixed the input completion popup sometimes disappearing when clicking on it on Windows and macOS. (#4876) - Bugfix: Fixed Twitch badges not loading correctly in the badge highlighting setting page. (#5223) - Bugfix: Fixed double-click text selection moving its position with each new message. (#4898) - Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899) - Bugfix: Fixed headers of tables in the settings switching to bold text when selected. (#4913) -- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172) - Bugfix: Fixed tooltips appearing too large and/or away from the cursor. (#4920) -- Bugfix: Fixed a crash when clicking `More messages below` button in a usercard and closing it quickly. (#4933) - Bugfix: Fixed thread popup window missing messages for nested threads. (#4923) - Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949) -- Bugfix: Fixed triple click on message also selecting moderation buttons. (#4961) -- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126) +- Bugfix: Fixed triple-click on message also selecting moderation buttons. (#4961) - Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110) - Bugfix: Fixed emotes being reloaded when pressing "Cancel" in the settings dialog, causing a slowdown. (#5240) +- Bugfix: Fixed double-click selection not correctly selecting words that were split onto multiple lines. (#5243) - Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126) -- Bugfix: Fixed double-click selection not selecting words that were split onto multiple lines correctly. (#5243) +- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126) - Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) -- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971) -- Bugfix: Fixed an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011) -- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972) +- Bugfix: Fixed a rare crash with the Image Uploader when closing a split right after starting an upload. (#4971) +- Bugfix: Fixed an issue on macOS where the Image Uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011) +- Bugfix: The usercard button is now hidden in the User Info Popup when in special channels. (#4972) - Bugfix: Fixed support for Windows 11 Snap layouts. (#4994, #5175) - Bugfix: Fixed some windows appearing between screens. (#4797) +- Bugfix: Fixed a crash that could occur when clicking `More messages below` button in a usercard and closing it quickly. (#4933) - Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051) - Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051) - Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040) - Bugfix: Fixes to section deletion in text input fields. (#5013) -- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) -- Bugfix: Show user text input within watch streak notices. (#5029) - Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052) -- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077) - Bugfix: Fixed an issue where you had to click the `reply` button twice if you already had that users @ in your input box. (#5173) - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) - Bugfix: Fixed link info not updating without moving the cursor. (#5178) - Bugfix: Fixed an upload sometimes failing when copying an image from a browser if it contained extra properties. (#5156) - Bugfix: Fixed tooltips getting out of bounds when loading images. (#5186) -- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) -- Bugfix: Detect when OBS is running on MacOS. (#5260) -- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) -- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) +- Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) +- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) +- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) +- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) From b6d75fd8672d7b00a3675b483e0f71e133788888 Mon Sep 17 00:00:00 2001 From: Maverick Date: Fri, 29 Mar 2024 20:50:43 +0100 Subject: [PATCH 18/33] feat: add more items in macOS menu bar (#5266) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/common/Common.hpp | 4 +++ src/widgets/Window.cpp | 37 ++++++++++++++++++++++++ src/widgets/dialogs/SettingsDialog.cpp | 7 ++++- src/widgets/dialogs/SettingsDialog.hpp | 1 + src/widgets/helper/SettingsDialogTab.hpp | 1 + src/widgets/settingspages/AboutPage.cpp | 3 +- 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8182382c46..cd6df89d280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215) - Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Added icons for newer versions of macOS. (#5148) +- Minor: Added more menu items in macOS menu bar. (#5266) - Minor: Improved color selection and display. (#5057) - Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118) - Minor: Normalized the input padding between light & dark themes. (#5095) diff --git a/src/common/Common.hpp b/src/common/Common.hpp index b0315a8aaf2..35b8efb1cc3 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -8,6 +8,10 @@ #include #include +#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com" +#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z" +#define LINK_CHATTERINO_SOURCE "https://github.com/Chatterino/chatterino2" + namespace chatterino { enum class HighlightState { diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 8b3cea43033..7106f872fcf 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/Args.hpp" +#include "common/Common.hpp" #include "common/Credentials.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" @@ -702,6 +703,14 @@ void Window::addMenuBar() // First menu. QMenu *menu = mainMenu->addMenu(QString()); + + // About button that shows the About tab in the Settings Dialog. + QAction *about = menu->addAction(QString()); + about->setMenuRole(QAction::AboutRole); + connect(about, &QAction::triggered, this, [this] { + SettingsDialog::showDialog(this, SettingsDialogPreference::About); + }); + QAction *prefs = menu->addAction(QString()); prefs->setMenuRole(QAction::PreferencesRole); connect(prefs, &QAction::triggered, this, [this] { @@ -711,6 +720,13 @@ void Window::addMenuBar() // Window menu. QMenu *windowMenu = mainMenu->addMenu(QString("Window")); + // Window->Minimize item + QAction *minimizeWindow = windowMenu->addAction(QString("Minimize")); + minimizeWindow->setShortcuts({QKeySequence("Meta+M")}); + connect(minimizeWindow, &QAction::triggered, this, [this] { + this->setWindowState(Qt::WindowMinimized); + }); + QAction *nextTab = windowMenu->addAction(QString("Select next tab")); nextTab->setShortcuts({QKeySequence("Meta+Tab")}); connect(nextTab, &QAction::triggered, this, [this] { @@ -722,6 +738,27 @@ void Window::addMenuBar() connect(prevTab, &QAction::triggered, this, [this] { this->notebook_->selectPreviousTab(); }); + + // Help menu. + QMenu *helpMenu = mainMenu->addMenu(QString("Help")); + + // Help->Chatterino Wiki item + QAction *helpWiki = helpMenu->addAction(QString("Chatterino Wiki")); + connect(helpWiki, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_WIKI)); + }); + + // Help->Chatterino Github + QAction *helpGithub = helpMenu->addAction(QString("Chatterino GitHub")); + connect(helpGithub, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_SOURCE)); + }); + + // Help->Chatterino Discord + QAction *helpDiscord = helpMenu->addAction(QString("Chatterino Discord")); + connect(helpDiscord, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_DISCORD)); + }); } void Window::onAccountSelected() diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 49cdf8e3539..62d459e222f 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -249,7 +249,7 @@ void SettingsDialog::addTabs() this->addTab([]{return new PluginsPage;}, "Plugins", ":/settings/plugins.svg"); #endif this->ui_.tabContainer->addStretch(1); - this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId(), Qt::AlignBottom); + this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId::About, Qt::AlignBottom); // clang-format on } @@ -366,6 +366,11 @@ void SettingsDialog::showDialog(QWidget *parent, } break; + case SettingsDialogPreference::About: { + instance->selectTab(SettingsTabId::About); + } + break; + default:; } diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index e227223de96..6c32e0ccbc3 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -30,6 +30,7 @@ enum class SettingsDialogPreference { StreamerMode, Accounts, ModerationActions, + About, }; class SettingsDialog : public BaseWindow diff --git a/src/widgets/helper/SettingsDialogTab.hpp b/src/widgets/helper/SettingsDialogTab.hpp index 97a1ad51d6d..0c60688b2aa 100644 --- a/src/widgets/helper/SettingsDialogTab.hpp +++ b/src/widgets/helper/SettingsDialogTab.hpp @@ -18,6 +18,7 @@ enum class SettingsTabId { General, Accounts, Moderation, + About, }; class SettingsDialogTab : public BaseWidget diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 471b6d03b25..78597c5fca3 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -1,5 +1,6 @@ #include "AboutPage.hpp" +#include "common/Common.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" @@ -18,10 +19,8 @@ #define PIXMAP_WIDTH 500 -#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com" #define LINK_DONATE "https://streamelements.com/fourtf/tip" #define LINK_CHATTERINO_FEATURES "https://chatterino.com/#features" -#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z" namespace chatterino { From 9583a10b88c191d6dbde6e3b08719d62118fed1f Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:24:09 +0100 Subject: [PATCH 19/33] fix(helix-chat): show better error messages (#5276) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchIrcServer.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6df89d280..37080a27be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,7 +186,7 @@ - Dev: Added signal to invalidate paint buffers of channel views without forcing a relayout. (#5123) - Dev: Specialize `Atomic>` if underlying standard library supports it. (#5133) - Dev: Added the `developer_name` field to the Linux AppData specification. (#5138) -- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200) +- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200, #5276) - Dev: Added estimation for image sizes to avoid layout shifts. (#5192) - Dev: Added the `launachable` entry to Linux AppData. (#5210) - Dev: Cleaned up and optimized resources. (#5222) diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 9a71c89ac77..f591e8f310e 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -39,9 +39,17 @@ const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; void sendHelixMessage(const std::shared_ptr &channel, const QString &message, const QString &replyParentId = {}) { + auto broadcasterID = channel->roomId(); + if (broadcasterID.isEmpty()) + { + channel->addMessage(makeSystemMessage( + "Sending messages in this channel isn't possible.")); + return; + } + getHelix()->sendChatMessage( { - .broadcasterID = channel->roomId(), + .broadcasterID = broadcasterID, .senderID = getIApp()->getAccounts()->twitch.getCurrent()->getUserId(), .message = message, @@ -68,13 +76,18 @@ void sendHelixMessage(const std::shared_ptr &channel, }(); chan->addMessage(errorMessage); }, - [weak = std::weak_ptr(channel)](auto error, const auto &message) { + [weak = std::weak_ptr(channel)](auto error, auto message) { auto chan = weak.lock(); if (!chan) { return; } + if (message.isEmpty()) + { + message = "(empty message)"; + } + using Error = decltype(error); auto errorMessage = [&]() -> QString { From 84e641d5892af7d8946acd015922006c0966bac6 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:25:11 +0100 Subject: [PATCH 20/33] ci: run clang-tidy with Qt 6 and update action (#5273) --- .CI/setup-clang-tidy.sh | 34 ++++++ .github/workflows/clang-tidy.yml | 113 +++---------------- .github/workflows/post-clang-tidy-review.yml | 5 +- CHANGELOG.md | 1 + 4 files changed, 53 insertions(+), 100 deletions(-) create mode 100755 .CI/setup-clang-tidy.sh diff --git a/.CI/setup-clang-tidy.sh b/.CI/setup-clang-tidy.sh new file mode 100755 index 00000000000..4884285eb8e --- /dev/null +++ b/.CI/setup-clang-tidy.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -ev; + +# aqt installs into .qtinstall/Qt//gcc_64 +# This is doing the same as jurplel/install-qt-action +# See https://github.com/jurplel/install-qt-action/blob/74ca8cd6681420fc8894aed264644c7a76d7c8cb/action/src/main.ts#L52-L74 +qtpath=$(echo .qtinstall/Qt/[0-9]*/*/bin/qmake | sed -e s:/bin/qmake$::) +export LD_LIBRARY_PATH="$qtpath/lib" +export QT_ROOT_DIR=$qtpath +export QT_PLUGIN_PATH="$qtpath/plugins" +export PATH="$PATH:$(realpath "$qtpath/bin")" +export Qt6_DIR="$(realpath "$qtpath")" + +cmake -S. -Bbuild-clang-tidy \ + -DCMAKE_BUILD_TYPE=Debug \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO=Off \ + -DCHATTERINO_PLUGINS=On \ + -DBUILD_WITH_QT6=On \ + -DBUILD_TESTS=On \ + -DBUILD_BENCHMARKS=On + +# Run MOC and UIC +# This will compile the dependencies +# Get the targets using `ninja -t targets | grep autogen` +cmake --build build-clang-tidy --parallel -t \ + Core_autogen \ + LibCommuni_autogen \ + Model_autogen \ + Util_autogen \ + chatterino-lib_autogen diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index b522bba5d3e..cf47eacaf11 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -8,60 +8,25 @@ concurrency: group: clang-tidy-${{ github.ref }} cancel-in-progress: true -env: - CHATTERINO_REQUIRE_CLEAN_GIT: On - C2_BUILD_WITH_QT6: Off - jobs: - build: + review: name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" runs-on: ${{ matrix.os }} strategy: matrix: include: - # Ubuntu 22.04, Qt 5.15 + # Ubuntu 22.04, Qt 6.6 - os: ubuntu-22.04 - qt-version: 5.15.2 - plugins: false + qt-version: 6.6.2 fail-fast: false steps: - - name: Enable plugin support - if: matrix.plugins - run: | - echo "C2_PLUGINS=ON" >> "$GITHUB_ENV" - shell: bash - - - name: Set BUILD_WITH_QT6 - if: startsWith(matrix.qt-version, '6.') - run: | - echo "C2_BUILD_WITH_QT6=ON" >> "$GITHUB_ENV" - shell: bash - - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 # allows for tags access - - name: Install Qt5 - if: startsWith(matrix.qt-version, '5.') - uses: jurplel/install-qt-action@v3.3.0 - with: - cache: true - cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 - version: ${{ matrix.qt-version }} - - - name: Install Qt 6.5.3 imageformats - if: startsWith(matrix.qt-version, '6.') - uses: jurplel/install-qt-action@v3.3.0 - with: - cache: false - modules: qtimageformats - set-env: false - version: 6.5.3 - extra: --noarchives - - name: Install Qt6 if: startsWith(matrix.qt-version, '6.') uses: jurplel/install-qt-action@v3.3.0 @@ -70,79 +35,31 @@ jobs: cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 modules: qt5compat qtimageformats version: ${{ matrix.qt-version }} - - # LINUX - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get -y install \ - cmake \ - virtualenv \ - rapidjson-dev \ - libfuse2 \ - libssl-dev \ - libboost-dev \ - libxcb-randr0-dev \ - libboost-system-dev \ - libboost-filesystem-dev \ - libpulse-dev \ - libxkbcommon-x11-0 \ - build-essential \ - libgl1-mesa-dev \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-render-util0 \ - libxcb-xinerama0 - - - name: Apply Qt5 patches - if: startsWith(matrix.qt-version, '5.') - run: | - patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch - shell: bash - - - name: Build - run: | - mkdir build - cd build - CXXFLAGS=-fno-sized-deallocation cmake \ - -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ - -DCMAKE_BUILD_TYPE=Release \ - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ - -DUSE_PRECOMPILED_HEADERS=OFF \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ - -DCHATTERINO_LTO="$C2_ENABLE_LTO" \ - -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ - -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ - .. - shell: bash + dir: ${{ github.workspace }}/.qtinstall + set-env: false - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.3 + uses: ZedThree/clang-tidy-review@v0.18.0 with: build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true exclude: "lib/*,tools/crash-handler/*" cmake_command: >- - cmake -S. -Bbuild-clang-tidy - -DCMAKE_BUILD_TYPE=Release - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On - -DUSE_PRECOMPILED_HEADERS=OFF - -DCMAKE_EXPORT_COMPILE_COMMANDS=On - -DCHATTERINO_LTO=Off - -DCHATTERINO_PLUGINS=On - -DBUILD_WITH_QT6=Off - -DBUILD_TESTS=On - -DBUILD_BENCHMARKS=On + ./.CI/setup-clang-tidy.sh apt_packages: >- - qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, libsecret-1-dev, libboost-dev, libboost-system-dev, libboost-filesystem-dev, libssl-dev, rapidjson-dev, - libbenchmark-dev + libbenchmark-dev, + build-essential, + libgl1-mesa-dev, libgstreamer-gl1.0-0, libpulse-dev, + libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, + libxcb-render-util0, libxcb-render0, libxcb-shape0, libxcb-shm0, libxcb-sync1, + libxcb-util1, libxcb-xfixes0, libxcb-xinerama0, libxcb1, libxkbcommon-dev, + libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0 - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.3 + uses: ZedThree/clang-tidy-review/upload@v0.18.0 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index 2f9b6b3d9fa..6c39a93a7ea 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -8,12 +8,13 @@ on: - completed jobs: - build: + post: runs-on: ubuntu-latest # Only when a build succeeds if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.3 + - uses: ZedThree/clang-tidy-review/post@v0.18.0 with: lgtm_comment_body: "" + num_comments_as_exitcode: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 37080a27be6..b8287d63a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,7 @@ - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) +- Dev: `clang-tidy` CI now uses Qt 6. (#5273) ## 2.4.6 From 09b2c53383af4e4e35f402a909573675d51e09ad Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:56:51 +0100 Subject: [PATCH 21/33] fix: rerender when unpausing (#5265) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8287d63a05..1ce0f4f60fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) +- Bugfix: Fixed pause indicator not disappearing in some cases. (#5265) - Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 1454b4999de..9c45c6d9ab1 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -536,6 +536,8 @@ void ChannelView::updatePauses() this->pauseScrollMaximumOffset_ = 0; this->queueLayout(); + // make sure we re-render + this->update(); } else if (std::any_of(this->pauses_.begin(), this->pauses_.end(), [](auto &&value) { @@ -560,8 +562,9 @@ void ChannelView::updatePauses() { /// Start the timer this->pauseEnd_ = pauseEnd; - this->pauseTimer_.start( - duration_cast(pauseEnd - SteadyClock::now())); + auto duration = + duration_cast(pauseEnd - SteadyClock::now()); + this->pauseTimer_.start(std::max(duration, 0ms)); } } } From 69bdac9936cd981679a8f5d2bf15b7873292b83f Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 12:28:49 +0100 Subject: [PATCH 22/33] Add `reward.cost` `reward.id`, `reward.title` filter variables (#5275) --- CHANGELOG.md | 1 + src/controllers/filters/lang/Filter.cpp | 12 ++++++++++++ src/controllers/filters/lang/Filter.hpp | 3 +++ src/controllers/filters/lang/Tokenizer.hpp | 6 +++++- src/messages/Message.hpp | 3 +++ src/providers/twitch/TwitchMessageBuilder.cpp | 2 ++ src/widgets/helper/ChannelView.cpp | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce0f4f60fa..b82b51b8a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) +- Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 7ae61991a90..ef0cfd15ce2 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -120,6 +120,18 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) vars["channel.live"] = false; } } + if (m->reward != nullptr) + { + vars["reward.title"] = m->reward->title; + vars["reward.cost"] = m->reward->cost; + vars["reward.id"] = m->reward->id; + } + else + { + vars["reward.title"] = ""; + vars["reward.cost"] = -1; + vars["reward.id"] = ""; + } return vars; } diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp index c8afbd76916..01d7a765e9d 100644 --- a/src/controllers/filters/lang/Filter.hpp +++ b/src/controllers/filters/lang/Filter.hpp @@ -48,6 +48,9 @@ static const QMap MESSAGE_TYPING_CONTEXT = { {"flags.monitored", Type::Bool}, {"message.content", Type::String}, {"message.length", Type::Int}, + {"reward.title", Type::String}, + {"reward.cost", Type::Int}, + {"reward.id", Type::String}, }; ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); diff --git a/src/controllers/filters/lang/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp index 2fbc5fd9536..6ca9d373ceb 100644 --- a/src/controllers/filters/lang/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -35,7 +35,11 @@ static const QMap validIdentifiersMap = { {"flags.restricted", "restricted message?"}, {"flags.monitored", "monitored message?"}, {"message.content", "message text"}, - {"message.length", "message length"}}; + {"message.length", "message length"}, + {"reward.title", "point reward title"}, + {"reward.cost", "point reward cost"}, + {"reward.id", "point reward id"}, +}; // clang-format off static const QRegularExpression tokenRegex( diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index b9e0b2321ec..bdbe120ddaa 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/FlagsEnum.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "util/QStringHash.hpp" #include @@ -107,6 +108,8 @@ struct Message { std::vector> elements; ScrollbarHighlight getScrollBarHighlight() const; + + std::shared_ptr reward = nullptr; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 88c0f671e74..524d0375de5 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1625,6 +1625,8 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( builder->message().messageText = textList.join(" "); builder->message().searchText = textList.join(" "); builder->message().loginName = reward.user.login; + + builder->message().reward = std::make_shared(reward); } void TwitchMessageBuilder::liveMessage(const QString &channelName, diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 9c45c6d9ab1..2795e5ffe7b 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -268,6 +268,20 @@ void addHiddenContextMenuItems(QMenu *menu, jsonObject["searchText"] = message->searchText; jsonObject["messageText"] = message->messageText; jsonObject["flags"] = qmagicenum::enumFlagsName(message->flags.value()); + if (message->reward) + { + QJsonObject reward; + reward["id"] = message->reward->id; + reward["title"] = message->reward->title; + reward["cost"] = message->reward->cost; + reward["isUserInputRequired"] = + message->reward->isUserInputRequired; + jsonObject["reward"] = reward; + } + else + { + jsonObject["reward"] = QJsonValue(); + } jsonDocument.setObject(jsonObject); From 2f534dc6dabe84c002ca4e54325a779a596980e9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 30 Mar 2024 14:24:47 +0100 Subject: [PATCH 23/33] fix: override broken base sizes & scales for some Twitch emotes (#5279) --- CHANGELOG.md | 1 + src/providers/twitch/TwitchEmotes.cpp | 407 +++++++++++++++++++++++++- src/providers/twitch/TwitchEmotes.hpp | 1 - 3 files changed, 398 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82b51b8a47..b3b0c05006d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279) - Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 4c87e472b61..918d504a46e 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -5,6 +5,400 @@ #include "messages/Image.hpp" #include "util/QStringHash.hpp" +namespace { + +using namespace chatterino; + +Url getEmoteLink(const EmoteId &id, const QString &emoteScale) +{ + return {QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", id.string) + .replace("{scale}", emoteScale)}; +} + +QSize getEmoteExpectedBaseSize(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr QSize defaultBaseSize(28, 28); + static std::unordered_map outliers{ + {"555555635", {21, 18}}, /* ;p */ + {"555555636", {21, 18}}, /* ;-p */ + {"555555614", {21, 18}}, /* O_o */ + {"555555641", {21, 18}}, /* :z */ + {"555555604", {21, 18}}, /* :\\ */ + {"444", {21, 18}}, /* :| */ + {"555555634", {21, 18}}, /* ;-P */ + {"439", {21, 18}}, /* ;) */ + {"555555642", {21, 18}}, /* :-z */ + {"555555613", {21, 18}}, /* :-o */ + {"555555625", {21, 18}}, /* :-p */ + {"433", {21, 18}}, /* :/ */ + {"555555622", {21, 18}}, /* :P */ + {"555555640", {21, 18}}, /* :-| */ + {"555555623", {21, 18}}, /* :-P */ + {"555555628", {21, 18}}, /* :) */ + {"555555632", {21, 18}}, /* 8-) */ + {"555555667", {20, 18}}, /* ;p */ + {"445", {21, 18}}, /* <3 */ + {"555555668", {20, 18}}, /* ;-p */ + {"555555679", {20, 18}}, /* :z */ + {"483", {20, 18}}, /* <3 */ + {"555555666", {20, 18}}, /* ;-P */ + {"497", {20, 18}}, /* O_o */ + {"555555664", {20, 18}}, /* :-p */ + {"555555671", {20, 18}}, /* :o */ + {"555555681", {20, 18}}, /* :Z */ + {"555555672", {20, 18}}, /* :-o */ + {"555555676", {20, 18}}, /* :-\\ */ + {"555555611", {21, 18}}, /* :-O */ + {"555555670", {20, 18}}, /* :-O */ + {"555555688", {20, 18}}, /* :-D */ + {"441", {21, 18}}, /* B) */ + {"555555601", {21, 18}}, /* >( */ + {"491", {20, 18}}, /* ;P */ + {"496", {20, 18}}, /* :D */ + {"492", {20, 18}}, /* :O */ + {"555555573", {24, 18}}, /* o_O */ + {"555555643", {21, 18}}, /* :Z */ + {"1898", {26, 28}}, /* ThunBeast */ + {"555555682", {20, 18}}, /* :-Z */ + {"1896", {20, 30}}, /* WholeWheat */ + {"1906", {24, 30}}, /* SoBayed */ + {"555555607", {21, 18}}, /* :-( */ + {"555555660", {20, 18}}, /* :-( */ + {"489", {20, 18}}, /* :( */ + {"495", {20, 18}}, /* :s */ + {"555555638", {21, 18}}, /* :-D */ + {"357", {28, 30}}, /* HotPokket */ + {"555555624", {21, 18}}, /* :p */ + {"73", {21, 30}}, /* DBstyle */ + {"555555674", {20, 18}}, /* :-/ */ + {"555555629", {21, 18}}, /* :-) */ + {"555555600", {24, 18}}, /* R-) */ + {"41", {19, 27}}, /* Kreygasm */ + {"555555612", {21, 18}}, /* :o */ + {"488", {29, 24}}, /* :7 */ + {"69", {41, 28}}, /* BloodTrail */ + {"555555608", {21, 18}}, /* R) */ + {"501", {20, 18}}, /* ;) */ + {"50", {18, 27}}, /* ArsonNoSexy */ + {"443", {21, 18}}, /* :D */ + {"1904", {24, 30}}, /* BigBrother */ + {"555555595", {24, 18}}, /* ;P */ + {"555555663", {20, 18}}, /* :p */ + {"555555576", {24, 18}}, /* o.o */ + {"360", {22, 30}}, /* FailFish */ + {"500", {20, 18}}, /* B) */ + {"3", {24, 18}}, /* :D */ + {"484", {20, 22}}, /* R) */ + {"555555678", {20, 18}}, /* :-| */ + {"7", {24, 18}}, /* B) */ + {"52", {32, 32}}, /* SMOrc */ + {"555555644", {21, 18}}, /* :-Z */ + {"18", {20, 27}}, /* TheRinger */ + {"49106", {27, 28}}, /* CorgiDerp */ + {"6", {24, 18}}, /* O_o */ + {"10", {24, 18}}, /* :/ */ + {"47", {24, 24}}, /* PunchTrees */ + {"555555561", {24, 18}}, /* :-D */ + {"555555564", {24, 18}}, /* :-| */ + {"13", {24, 18}}, /* ;P */ + {"555555593", {24, 18}}, /* :p */ + {"555555589", {24, 18}}, /* ;) */ + {"555555590", {24, 18}}, /* ;-) */ + {"486", {27, 42}}, /* :> */ + {"40", {21, 27}}, /* KevinTurtle */ + {"555555558", {24, 18}}, /* :( */ + {"555555597", {24, 18}}, /* ;p */ + {"555555580", {24, 18}}, /* :O */ + {"555555567", {24, 18}}, /* :Z */ + {"1", {24, 18}}, /* :) */ + {"11", {24, 18}}, /* ;) */ + {"33", {25, 32}}, /* DansGame */ + {"555555586", {24, 18}}, /* :-/ */ + {"4", {24, 18}}, /* >( */ + {"555555588", {24, 18}}, /* :-\\ */ + {"12", {24, 18}}, /* :P */ + {"555555563", {24, 18}}, /* :| */ + {"555555581", {24, 18}}, /* :-O */ + {"555555598", {24, 18}}, /* ;-p */ + {"555555596", {24, 18}}, /* ;-P */ + {"555555557", {24, 18}}, /* :-) */ + {"498", {20, 18}}, /* >( */ + {"555555680", {20, 18}}, /* :-z */ + {"555555587", {24, 18}}, /* :\\ */ + {"5", {24, 18}}, /* :| */ + {"354", {20, 30}}, /* 4Head */ + {"555555562", {24, 18}}, /* >( */ + {"555555594", {24, 18}}, /* :-p */ + {"490", {20, 18}}, /* :P */ + {"555555662", {20, 18}}, /* :-P */ + {"2", {24, 18}}, /* :( */ + {"1902", {27, 29}}, /* Keepo */ + {"555555627", {21, 18}}, /* ;-) */ + {"555555566", {24, 18}}, /* :-z */ + {"555555559", {24, 18}}, /* :-( */ + {"555555592", {24, 18}}, /* :-P */ + {"28", {39, 27}}, /* MrDestructoid */ + {"8", {24, 18}}, /* :O */ + {"244", {24, 30}}, /* FUNgineer */ + {"555555591", {24, 18}}, /* :P */ + {"555555585", {24, 18}}, /* :/ */ + {"494", {20, 18}}, /* :| */ + {"9", {24, 18}}, /* <3 */ + {"555555584", {24, 18}}, /* <3 */ + {"555555579", {24, 18}}, /* 8-) */ + {"14", {24, 18}}, /* R) */ + {"485", {27, 18}}, /* #/ */ + {"555555560", {24, 18}}, /* :D */ + {"86", {36, 30}}, /* BibleThump */ + {"555555578", {24, 18}}, /* B-) */ + {"17", {20, 27}}, /* StoneLightning */ + {"436", {21, 18}}, /* :O */ + {"555555675", {20, 18}}, /* :\\ */ + {"22", {19, 27}}, /* RedCoat */ + {"555555574", {24, 18}}, /* o.O */ + {"555555603", {21, 18}}, /* :-/ */ + {"1901", {24, 28}}, /* Kippa */ + {"15", {21, 27}}, /* JKanStyle */ + {"555555605", {21, 18}}, /* :-\\ */ + {"555555701", {20, 18}}, /* ;-) */ + {"487", {20, 42}}, /* <] */ + {"555555572", {24, 18}}, /* O.O */ + {"65", {40, 30}}, /* FrankerZ */ + {"25", {25, 28}}, /* Kappa */ + {"36", {36, 30}}, /* PJSalt */ + {"499", {20, 18}}, /* :) */ + {"555555565", {24, 18}}, /* :z */ + {"434", {21, 18}}, /* :( */ + {"555555577", {24, 18}}, /* B) */ + {"34", {21, 28}}, /* SwiftRage */ + {"555555575", {24, 18}}, /* o_o */ + {"92", {23, 30}}, /* PMSTwin */ + {"555555570", {24, 18}}, /* O.o */ + {"555555569", {24, 18}}, /* O_o */ + {"493", {20, 18}}, /* :/ */ + {"26", {20, 27}}, /* JonCarnage */ + {"66", {20, 27}}, /* OneHand */ + {"555555568", {24, 18}}, /* :-Z */ + {"555555599", {24, 18}}, /* R) */ + {"1900", {33, 30}}, /* RalpherZ */ + {"555555582", {24, 18}}, /* :o */ + {"1899", {22, 30}}, /* TF2John */ + {"555555633", {21, 18}}, /* ;P */ + {"16", {22, 27}}, /* OptimizePrime */ + {"30", {29, 27}}, /* BCWarrior */ + {"555555583", {24, 18}}, /* :-o */ + {"32", {21, 27}}, /* GingerPower */ + {"87", {24, 30}}, /* ShazBotstix */ + {"74", {24, 30}}, /* AsianGlow */ + {"555555571", {24, 18}}, /* O_O */ + {"46", {24, 24}}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return defaultBaseSize; +} + +qreal getEmote3xScaleFactor(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr qreal default3xScaleFactor = 0.25; + static std::unordered_map outliers{ + {"555555635", 0.3333333333333333}, /* ;p */ + {"555555636", 0.3333333333333333}, /* ;-p */ + {"555555614", 0.3333333333333333}, /* O_o */ + {"555555641", 0.3333333333333333}, /* :z */ + {"555555604", 0.3333333333333333}, /* :\\ */ + {"444", 0.3333333333333333}, /* :| */ + {"555555634", 0.3333333333333333}, /* ;-P */ + {"439", 0.3333333333333333}, /* ;) */ + {"555555642", 0.3333333333333333}, /* :-z */ + {"555555613", 0.3333333333333333}, /* :-o */ + {"555555625", 0.3333333333333333}, /* :-p */ + {"433", 0.3333333333333333}, /* :/ */ + {"555555622", 0.3333333333333333}, /* :P */ + {"555555640", 0.3333333333333333}, /* :-| */ + {"555555623", 0.3333333333333333}, /* :-P */ + {"555555628", 0.3333333333333333}, /* :) */ + {"555555632", 0.3333333333333333}, /* 8-) */ + {"555555667", 0.3333333333333333}, /* ;p */ + {"445", 0.3333333333333333}, /* <3 */ + {"555555668", 0.3333333333333333}, /* ;-p */ + {"555555679", 0.3333333333333333}, /* :z */ + {"483", 0.3333333333333333}, /* <3 */ + {"555555666", 0.3333333333333333}, /* ;-P */ + {"497", 0.3333333333333333}, /* O_o */ + {"555555664", 0.3333333333333333}, /* :-p */ + {"555555671", 0.3333333333333333}, /* :o */ + {"555555681", 0.3333333333333333}, /* :Z */ + {"555555672", 0.3333333333333333}, /* :-o */ + {"555555676", 0.3333333333333333}, /* :-\\ */ + {"555555611", 0.3333333333333333}, /* :-O */ + {"555555670", 0.3333333333333333}, /* :-O */ + {"555555688", 0.3333333333333333}, /* :-D */ + {"441", 0.3333333333333333}, /* B) */ + {"555555601", 0.3333333333333333}, /* >( */ + {"491", 0.3333333333333333}, /* ;P */ + {"496", 0.3333333333333333}, /* :D */ + {"492", 0.3333333333333333}, /* :O */ + {"555555573", 0.3333333333333333}, /* o_O */ + {"555555643", 0.3333333333333333}, /* :Z */ + {"1898", 0.3333333333333333}, /* ThunBeast */ + {"555555682", 0.3333333333333333}, /* :-Z */ + {"1896", 0.3333333333333333}, /* WholeWheat */ + {"1906", 0.3333333333333333}, /* SoBayed */ + {"555555607", 0.3333333333333333}, /* :-( */ + {"555555660", 0.3333333333333333}, /* :-( */ + {"489", 0.3333333333333333}, /* :( */ + {"495", 0.3333333333333333}, /* :s */ + {"555555638", 0.3333333333333333}, /* :-D */ + {"357", 0.3333333333333333}, /* HotPokket */ + {"555555624", 0.3333333333333333}, /* :p */ + {"73", 0.3333333333333333}, /* DBstyle */ + {"555555674", 0.3333333333333333}, /* :-/ */ + {"555555629", 0.3333333333333333}, /* :-) */ + {"555555600", 0.3333333333333333}, /* R-) */ + {"41", 0.3333333333333333}, /* Kreygasm */ + {"555555612", 0.3333333333333333}, /* :o */ + {"488", 0.3333333333333333}, /* :7 */ + {"69", 0.3333333333333333}, /* BloodTrail */ + {"555555608", 0.3333333333333333}, /* R) */ + {"501", 0.3333333333333333}, /* ;) */ + {"50", 0.3333333333333333}, /* ArsonNoSexy */ + {"443", 0.3333333333333333}, /* :D */ + {"1904", 0.3333333333333333}, /* BigBrother */ + {"555555595", 0.3333333333333333}, /* ;P */ + {"555555663", 0.3333333333333333}, /* :p */ + {"555555576", 0.3333333333333333}, /* o.o */ + {"360", 0.3333333333333333}, /* FailFish */ + {"500", 0.3333333333333333}, /* B) */ + {"3", 0.3333333333333333}, /* :D */ + {"484", 0.3333333333333333}, /* R) */ + {"555555678", 0.3333333333333333}, /* :-| */ + {"7", 0.3333333333333333}, /* B) */ + {"52", 0.3333333333333333}, /* SMOrc */ + {"555555644", 0.3333333333333333}, /* :-Z */ + {"18", 0.3333333333333333}, /* TheRinger */ + {"49106", 0.3333333333333333}, /* CorgiDerp */ + {"6", 0.3333333333333333}, /* O_o */ + {"10", 0.3333333333333333}, /* :/ */ + {"47", 0.3333333333333333}, /* PunchTrees */ + {"555555561", 0.3333333333333333}, /* :-D */ + {"555555564", 0.3333333333333333}, /* :-| */ + {"13", 0.3333333333333333}, /* ;P */ + {"555555593", 0.3333333333333333}, /* :p */ + {"555555589", 0.3333333333333333}, /* ;) */ + {"555555590", 0.3333333333333333}, /* ;-) */ + {"486", 0.3333333333333333}, /* :> */ + {"40", 0.3333333333333333}, /* KevinTurtle */ + {"555555558", 0.3333333333333333}, /* :( */ + {"555555597", 0.3333333333333333}, /* ;p */ + {"555555580", 0.3333333333333333}, /* :O */ + {"555555567", 0.3333333333333333}, /* :Z */ + {"1", 0.3333333333333333}, /* :) */ + {"11", 0.3333333333333333}, /* ;) */ + {"33", 0.3333333333333333}, /* DansGame */ + {"555555586", 0.3333333333333333}, /* :-/ */ + {"4", 0.3333333333333333}, /* >( */ + {"555555588", 0.3333333333333333}, /* :-\\ */ + {"12", 0.3333333333333333}, /* :P */ + {"555555563", 0.3333333333333333}, /* :| */ + {"555555581", 0.3333333333333333}, /* :-O */ + {"555555598", 0.3333333333333333}, /* ;-p */ + {"555555596", 0.3333333333333333}, /* ;-P */ + {"555555557", 0.3333333333333333}, /* :-) */ + {"498", 0.3333333333333333}, /* >( */ + {"555555680", 0.3333333333333333}, /* :-z */ + {"555555587", 0.3333333333333333}, /* :\\ */ + {"5", 0.3333333333333333}, /* :| */ + {"354", 0.3333333333333333}, /* 4Head */ + {"555555562", 0.3333333333333333}, /* >( */ + {"555555594", 0.3333333333333333}, /* :-p */ + {"490", 0.3333333333333333}, /* :P */ + {"555555662", 0.3333333333333333}, /* :-P */ + {"2", 0.3333333333333333}, /* :( */ + {"1902", 0.3333333333333333}, /* Keepo */ + {"555555627", 0.3333333333333333}, /* ;-) */ + {"555555566", 0.3333333333333333}, /* :-z */ + {"555555559", 0.3333333333333333}, /* :-( */ + {"555555592", 0.3333333333333333}, /* :-P */ + {"28", 0.3333333333333333}, /* MrDestructoid */ + {"8", 0.3333333333333333}, /* :O */ + {"244", 0.3333333333333333}, /* FUNgineer */ + {"555555591", 0.3333333333333333}, /* :P */ + {"555555585", 0.3333333333333333}, /* :/ */ + {"494", 0.3333333333333333}, /* :| */ + {"9", 0.21428571428571427}, /* <3 */ + {"555555584", 0.21428571428571427}, /* <3 */ + {"555555579", 0.3333333333333333}, /* 8-) */ + {"14", 0.3333333333333333}, /* R) */ + {"485", 0.3333333333333333}, /* #/ */ + {"555555560", 0.3333333333333333}, /* :D */ + {"86", 0.3333333333333333}, /* BibleThump */ + {"555555578", 0.3333333333333333}, /* B-) */ + {"17", 0.3333333333333333}, /* StoneLightning */ + {"436", 0.3333333333333333}, /* :O */ + {"555555675", 0.3333333333333333}, /* :\\ */ + {"22", 0.3333333333333333}, /* RedCoat */ + {"245", 0.3333333333333333}, /* ResidentSleeper */ + {"555555574", 0.3333333333333333}, /* o.O */ + {"555555603", 0.3333333333333333}, /* :-/ */ + {"1901", 0.3333333333333333}, /* Kippa */ + {"15", 0.3333333333333333}, /* JKanStyle */ + {"555555605", 0.3333333333333333}, /* :-\\ */ + {"555555701", 0.3333333333333333}, /* ;-) */ + {"487", 0.3333333333333333}, /* <] */ + {"22639", 0.3333333333333333}, /* BabyRage */ + {"555555572", 0.3333333333333333}, /* O.O */ + {"65", 0.3333333333333333}, /* FrankerZ */ + {"25", 0.3333333333333333}, /* Kappa */ + {"36", 0.3333333333333333}, /* PJSalt */ + {"499", 0.3333333333333333}, /* :) */ + {"555555565", 0.3333333333333333}, /* :z */ + {"434", 0.3333333333333333}, /* :( */ + {"555555577", 0.3333333333333333}, /* B) */ + {"34", 0.3333333333333333}, /* SwiftRage */ + {"555555575", 0.3333333333333333}, /* o_o */ + {"92", 0.3333333333333333}, /* PMSTwin */ + {"555555570", 0.3333333333333333}, /* O.o */ + {"555555569", 0.3333333333333333}, /* O_o */ + {"493", 0.3333333333333333}, /* :/ */ + {"26", 0.3333333333333333}, /* JonCarnage */ + {"66", 0.3333333333333333}, /* OneHand */ + {"973", 0.3333333333333333}, /* DAESuppy */ + {"555555568", 0.3333333333333333}, /* :-Z */ + {"555555599", 0.3333333333333333}, /* R) */ + {"1900", 0.3333333333333333}, /* RalpherZ */ + {"555555582", 0.3333333333333333}, /* :o */ + {"1899", 0.3333333333333333}, /* TF2John */ + {"555555633", 0.3333333333333333}, /* ;P */ + {"16", 0.3333333333333333}, /* OptimizePrime */ + {"30", 0.3333333333333333}, /* BCWarrior */ + {"555555583", 0.3333333333333333}, /* :-o */ + {"32", 0.3333333333333333}, /* GingerPower */ + {"87", 0.3333333333333333}, /* ShazBotstix */ + {"74", 0.3333333333333333}, /* AsianGlow */ + {"555555571", 0.3333333333333333}, /* O_O */ + {"46", 0.3333333333333333}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return default3xScaleFactor; +} + +} // namespace + namespace chatterino { QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode) @@ -44,14 +438,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { - // From Twitch docs - expected size for an emote (1x) - constexpr QSize baseSize(28, 28); + auto baseSize = getEmoteExpectedBaseSize(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), - Image::fromUrl(getEmoteLink(id, "3.0"), 0.25, baseSize * 4), + Image::fromUrl(getEmoteLink(id, "3.0"), + getEmote3xScaleFactor(id), baseSize * 4), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); @@ -60,11 +454,4 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, return shared; } -Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) -{ - return {QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", id.string) - .replace("{scale}", emoteScale)}; -} - } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index d793ce72338..17e50b11fcb 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -52,7 +52,6 @@ class TwitchEmotes : public ITwitchEmotes const EmoteName &name) override; private: - Url getEmoteLink(const EmoteId &id, const QString &emoteScale); UniqueAccess>> twitchEmotesCache_; }; From b35f10fa540562e8c524ec31ae51e3d4fb139214 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 14:50:58 +0100 Subject: [PATCH 24/33] chore: require newline at EOF (#5278) --- .clang-format | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.clang-format b/.clang-format index 0feaad9dc10..cfbe49d31fe 100644 --- a/.clang-format +++ b/.clang-format @@ -50,3 +50,4 @@ PointerBindsToType: false SpacesBeforeTrailingComments: 2 Standard: Auto ReflowComments: false +InsertNewlineAtEOF: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b0c05006d..d89ebd1eec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,6 +198,7 @@ - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) - Dev: `clang-tidy` CI now uses Qt 6. (#5273) +- Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) ## 2.4.6 From d4b8feac7d86f941c09c4a1f54743d2882d9a54a Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 15:23:02 +0100 Subject: [PATCH 25/33] lua: Change CompletionRequested handler to use an event table. (#5280) --- CHANGELOG.md | 1 + docs/chatterino.d.ts | 14 ++++++----- docs/wip-plugins.md | 6 ++--- src/controllers/plugins/LuaAPI.hpp | 26 +++++++++++++++++++- src/controllers/plugins/LuaUtilities.cpp | 14 +++++++++++ src/controllers/plugins/LuaUtilities.hpp | 2 ++ src/controllers/plugins/Plugin.hpp | 6 ++--- src/controllers/plugins/PluginController.cpp | 8 ++++-- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89ebd1eec1..f9ec7507035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) +- Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 9bf6f57c0fb..95d2282be9f 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -75,6 +75,13 @@ declare module c2 { handler: (ctx: CommandContext) => void ): boolean; + class CompletionEvent { + query: string; + full_text_content: string; + cursor_position: number; + is_first_word: boolean; + } + class CompletionList { values: String[]; hide_others: boolean; @@ -84,12 +91,7 @@ declare module c2 { CompletionRequested = "CompletionRequested", } - type CbFuncCompletionsRequested = ( - query: string, - full_text_content: string, - cursor_position: number, - is_first_word: boolean - ) => CompletionList; + type CbFuncCompletionsRequested = (ev: CompletionEvent) => CompletionList; type CbFunc = T extends EventType.CompletionRequested ? CbFuncCompletionsRequested : never; diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 1309d7bab0c..32eda387fd1 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -167,7 +167,7 @@ Limitations/known issues: #### `register_callback("CompletionRequested", handler)` -Registers a callback (`handler`) to process completions. The callback gets the following parameters: +Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries: - `query`: The queried word. - `full_text_content`: The whole input. @@ -190,8 +190,8 @@ end c2.register_callback( "CompletionRequested", - function(query, full_text_content, cursor_position, is_first_word) - if ("!join"):startswith(query) then + function(event) + if ("!join"):startswith(event.query) then ---@type CompletionList return { hide_others = true, values = { "!join" } } end diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index df042b24f5c..39df152169a 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -5,6 +5,8 @@ extern "C" { # include } +# include "controllers/plugins/LuaUtilities.hpp" + # include # include @@ -55,6 +57,28 @@ struct CompletionList { bool hideOthers{}; }; +/** + * @lua@class CompletionEvent + */ +struct CompletionEvent { + /** + * @lua@field query string The word being completed + */ + QString query; + /** + * @lua@field full_text_content string Content of the text input + */ + QString full_text_content; + /** + * @lua@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) + */ + int cursor_position{}; + /** + * @lua@field is_first_word boolean True if this is the first word in the input + */ + bool is_first_word{}; +}; + /** * @includefile common/Channel.hpp * @includefile controllers/plugins/api/ChannelRef.hpp @@ -74,7 +98,7 @@ int c2_register_command(lua_State *L); * Registers a callback to be invoked when completions for a term are requested. * * @lua@param type "CompletionRequested" - * @lua@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. + * @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. * @exposed c2.register_callback */ int c2_register_callback(lua_State *L); diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index 9361cd1ff3e..64af18c0133 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -142,6 +142,20 @@ StackIdx push(lua_State *L, const int &b) return lua_gettop(L); } +StackIdx push(lua_State *L, const api::CompletionEvent &ev) +{ + auto idx = pushEmptyTable(L, 4); +# define PUSH(field) \ + lua::push(L, ev.field); \ + lua_setfield(L, idx, #field) + PUSH(query); + PUSH(full_text_content); + PUSH(cursor_position); + PUSH(is_first_word); +# undef PUSH + return idx; +} + bool peek(lua_State *L, int *out, StackIdx idx) { StackGuard guard(L); diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 4c78d6edc9a..5443a751f79 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -28,6 +28,7 @@ namespace chatterino::lua { namespace api { struct CompletionList; + struct CompletionEvent; } // namespace api constexpr int ERROR_BAD_PEEK = LUA_OK - 1; @@ -66,6 +67,7 @@ StackIdx push(lua_State *L, const QString &str); StackIdx push(lua_State *L, const std::string &str); StackIdx push(lua_State *L, const bool &b); StackIdx push(lua_State *L, const int &b); +StackIdx push(lua_State *L, const api::CompletionEvent &ev); // returns OK? bool peek(lua_State *L, int *out, StackIdx idx = -1); diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 4450b2a0192..2adbe9067fb 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -98,8 +98,8 @@ class Plugin // Note: The CallbackFunction object's destructor will remove the function from the lua stack using LuaCompletionCallback = - lua::CallbackFunction; + lua::CallbackFunction; std::optional getCompletionCallback() { if (this->state_ == nullptr || !this->error_.isNull()) @@ -123,7 +123,7 @@ class Plugin // move return std::make_optional>( + lua::api::CompletionList, lua::api::CompletionEvent>>( this->state_, lua_gettop(this->state_)); } diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 0f23df3430d..8c2d8055619 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -433,8 +433,12 @@ std::pair PluginController::updateCustomCompletions( qCDebug(chatterinoLua) << "Processing custom completions from plugin" << name; auto &cb = *opt; - auto errOrList = - cb(query, fullTextContent, cursorPosition, isFirstWord); + auto errOrList = cb(lua::api::CompletionEvent{ + .query = query, + .full_text_content = fullTextContent, + .cursor_position = cursorPosition, + .is_first_word = isFirstWord, + }); if (std::holds_alternative(errOrList)) { guard.handled(); From c1bd5d11d057ae15e8866511bdc9645466e5ec68 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 22:11:52 +0100 Subject: [PATCH 26/33] refactor: improve LuaLS generator (#5283) --- CHANGELOG.md | 2 +- docs/plugin-meta.lua | 71 ++-- scripts/make_luals_meta.py | 385 ++++++++++++++------- src/controllers/plugins/LuaAPI.hpp | 2 +- src/controllers/plugins/api/ChannelRef.hpp | 11 +- 5 files changed, 307 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ec7507035..01c49ca3401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -179,7 +179,7 @@ - Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747) - Dev: Refactor Args to be less of a singleton. (#5041) - Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045) -- Dev: Autogenerate docs/plugin-meta.lua. (#5055) +- Dev: Autogenerate docs/plugin-meta.lua. (#5055, #5283) - Dev: Changed Ubuntu & AppImage builders to statically link Qt. (#5151) - Dev: Refactor `NetworkPrivate`. (#5063) - Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 2cc56af5994..7b72b46d5f3 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -6,22 +6,14 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - - ----@alias LogLevel integer ----@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel } +---@alias c2.LogLevel integer +---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } c2.LogLevel = {} ----@alias EventType integer ----@type { CompletionRequested: EventType } +---@alias c2.EventType integer +---@type { CompletionRequested: c2.EventType } c2.EventType = {} + ---@class CommandContext ---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. ---@field channel Channel The channel the command was executed in. @@ -29,20 +21,31 @@ c2.EventType = {} ---@class CompletionList ---@field values string[] The completions ---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. --- Now including data from src/common/Channel.hpp. + +---@class CompletionEvent +---@field query string The word being completed +---@field full_text_content string Content of the text input +---@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) +---@field is_first_word boolean True if this is the first word in the input + +-- Begin src/common/Channel.hpp ---@alias ChannelType integer ----@type { None: ChannelType } +---@type { None: ChannelType, Direct: ChannelType, Twitch: ChannelType, TwitchWhispers: ChannelType, TwitchWatching: ChannelType, TwitchMentions: ChannelType, TwitchLive: ChannelType, TwitchAutomod: ChannelType, TwitchEnd: ChannelType, Irc: ChannelType, Misc: ChannelType } ChannelType = {} --- Back to src/controllers/plugins/LuaAPI.hpp. --- Now including data from src/controllers/plugins/api/ChannelRef.hpp. ---- This enum describes a platform for the purpose of searching for a channel. ---- Currently only Twitch is supported because identifying IRC channels is tricky. + +-- End src/common/Channel.hpp + +-- Begin src/controllers/plugins/api/ChannelRef.hpp ---@alias Platform integer +--- This enum describes a platform for the purpose of searching for a channel. +--- Currently only Twitch is supported because identifying IRC channels is tricky. ---@type { Twitch: Platform } Platform = {} ----@class Channel: IWeakResource + +---@class Channel +Channel = {} --- Returns true if the channel this object points to is valid. --- If the object expired, returns false @@ -82,11 +85,9 @@ function Channel:add_system_message(message) end --- Compares the channel Type. Note that enum values aren't guaranteed, just --- that they are equal to the exposed enum. --- ----@return bool +---@return boolean function Channel:is_twitch_channel() end ---- Twitch Channel specific functions - --- Returns a copy of the channel mode settings (subscriber only, r9k etc.) --- ---@return RoomModes @@ -119,15 +120,10 @@ function Channel:is_mod() end ---@return boolean function Channel:is_vip() end ---- Misc - ---@return string function Channel:__tostring() end ---- Static functions - --- Finds a channel by name. ---- --- Misc channels are marked as Twitch: --- - /whispers --- - /mentions @@ -142,19 +138,15 @@ function Channel.by_name(name, platform) end --- Finds a channel by the Twitch user ID of its owner. --- ----@param string id ID of the owner of the channel. +---@param id string ID of the owner of the channel. ---@return Channel? -function Channel.by_twitch_id(string) end +function Channel.by_twitch_id(id) end ---@class RoomModes ---@field unique_chat boolean You might know this as r9kbeta or robot9000. ---@field subscriber_only boolean ----@field emotes_only boolean Whether or not text is allowed in messages. - ---- Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes - ----@field unique_chat number? Time in minutes you need to follow to chat or nil. - +---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes +---@field follower_only number? Time in minutes you need to follow to chat or nil. ---@field slow_mode number? Time in seconds you need to wait before sending messages or nil. ---@class StreamStatus @@ -164,7 +156,8 @@ function Channel.by_twitch_id(string) end ---@field title string Stream title or last stream title ---@field game_name string ---@field game_id string --- Back to src/controllers/plugins/LuaAPI.hpp. + +-- End src/controllers/plugins/api/ChannelRef.hpp --- Registers a new command called `name` which when executed will call `handler`. --- @@ -176,12 +169,12 @@ function c2.register_command(name, handler) end --- Registers a callback to be invoked when completions for a term are requested. --- ---@param type "CompletionRequested" ----@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. +---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. function c2.register_callback(type, func) end --- Writes a message to the Chatterino log. --- ----@param level LogLevel The desired level. +---@param level c2.LogLevel The desired level. ---@param ... any Values to log. Should be convertible to a string with `tostring()`. function c2.log(level, ...) end diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index 22240da1984..58a06242806 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -12,25 +12,26 @@ - Do not have any useful info on '/**' and '*/' lines. - Class members are not allowed to have non-@command lines and commands different from @lua@field -When this scripts sees "@brief", any further lines of the comment will be ignored +Only entire comment blocks are used. One comment block can describe at most one +entity (function/class/enum). Blocks without commands are ignored. Valid commands are: 1. @exposeenum [dotted.name.in_lua.last_part] Define a table with keys of the enum. Values behind those keys aren't written on purpose. - This generates three lines: - - An type alias of [last_part] to integer, - - A type description that describes available values of the enum, - - A global table definition for the num -2. @lua[@command] +2. @exposed [c2.name] + Generates a function definition line from the last `@lua@param`s. +3. @lua[@command] Writes [@command] to the file as a comment, usually this is @class, @param, @return, ... @lua@class and @lua@field have special treatment when it comes to generation of spacing new lines -3. @exposed [c2.name] - Generates a function definition line from the last `@lua@param`s. Non-command lines of comments are written with a space after '---' """ + +from io import TextIOWrapper from pathlib import Path +import re +from typing import Optional BOILERPLATE = """ ---@meta Chatterino2 @@ -41,14 +42,6 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - """ repo_root = Path(__file__).parent.parent @@ -58,116 +51,274 @@ print("Writing to", lua_meta.relative_to(repo_root)) -def process_file(target, out): - print("Reading from", target.relative_to(repo_root)) - with target.open("r") as f: - lines = f.read().splitlines() +def strip_line(line: str): + return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() - # Are we in a doc comment? - comment: bool = False - # This is set when @brief is encountered, making the rest of the comment be - # ignored - ignore_this_comment: bool = False - - # Last `@lua@param`s seen - for @exposed generation - last_params_names: list[str] = [] - # Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier - is_class = False - - # The name of the next enum in lua world - expose_next_enum_as: str | None = None - # Name of the current enum in c++ world, used to generate internal typenames for - current_enum_name: str | None = None - for line_num, line in enumerate(lines): - line = line.strip() - loc = f'{target.relative_to(repo_root)}:{line_num}' - if line.startswith("enum class "): - line = line.removeprefix("enum class ") - temp = line.split(" ", 2) - current_enum_name = temp[0] - if not expose_next_enum_as: - print( - f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command" - ) - current_enum_name = None + +def is_comment_start(line: str): + return line.startswith("/**") + + +def is_enum_class(line: str): + return line.startswith("enum class") + + +def is_class(line: str): + return line.startswith(("class", "struct")) + + +class Reader: + lines: list[str] + line_idx: int + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self.line_idx = 0 + + def line_no(self) -> int: + """Returns the current line number (starting from 1)""" + return self.line_idx + 1 + + def has_next(self) -> bool: + """Returns true if there are lines left to read""" + return self.line_idx < len(self.lines) + + def peek_line(self) -> Optional[str]: + """Reads the line the cursor is at""" + if self.has_next(): + return self.lines[self.line_idx].strip() + return None + + def next_line(self) -> Optional[str]: + """Consumes and returns one line""" + if self.has_next(): + self.line_idx += 1 + return self.lines[self.line_idx - 1].strip() + return None + + def next_doc_comment(self) -> Optional[list[str]]: + """Reads a documentation comment (/** ... */) and advances the cursor""" + lines = [] + # find the start + while (line := self.next_line()) is not None and not is_comment_start(line): + pass + if line is None: + return None + + stripped = strip_line(line) + if stripped: + lines.append(stripped) + + if stripped.endswith("*/"): + return lines if lines else None + + while (line := self.next_line()) is not None: + if line.startswith("*/"): + break + + stripped = strip_line(line) + if not stripped: continue - current_enum_name = expose_next_enum_as.split(".", 1)[-1] - out.write("---@alias " + current_enum_name + " integer\n") - out.write("---@type { ") - # temp[1] is '{' - if len(temp) == 2: # no values on this line + + if stripped.startswith("@"): + lines.append(stripped) + continue + + if not lines: + lines.append(stripped) + else: + lines[-1] += "\n--- " + stripped + + return lines if lines else None + + def read_class_body(self) -> list[list[str]]: + """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" + items = [] + while (line := self.peek_line()) is not None: + if line.startswith("};"): + self.next_line() + break + if not is_comment_start(line): + self.next_line() + continue + doc = self.next_doc_comment() + if not doc: + break + items.append(doc) + return items + + def read_enum_variants(self) -> list[str]: + """The reader must be before an enum class definition (possibly with some comments before). It returns all variants.""" + items = [] + is_comment = False + while (line := self.peek_line()) is not None and not line.startswith("};"): + self.next_line() + if is_comment: + if line.endswith("*/"): + is_comment = False + continue + if line.startswith("/*"): + is_comment = True + continue + if line.startswith("//"): continue - line = temp[2] - - if current_enum_name is not None: - for i, tok in enumerate(line.split(" ")): - if tok == "};": - break - entry = tok.removesuffix(",") - if i != 0: - out.write(", ") - out.write(entry + ": " + current_enum_name) - out.write(" }\n" f"{expose_next_enum_as} = {{}}\n") - print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}") - current_enum_name = None - expose_next_enum_as = None + if line.endswith("};"): # oneline declaration + opener = line.find("{") + 1 + closer = line.find("}") + items = [ + line.split("=", 1)[0].strip() + for line in line[opener:closer].split(",") + ] + break + if line.startswith("enum class"): + continue + + items.append(line.rstrip(",")) + + return items + + +def finish_class(out, name): + out.write(f"{name} = {{}}\n") + + +def printmsg(path: Path, line: int, message: str): + print(f"{path.relative_to(repo_root)}:{line} {message}") + + +def panic(path: Path, line: int, message: str): + printmsg(path, line, message) + exit(1) + + +def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): + if not comments[0].startswith("@"): + out.write(f"--- {comments[0]}\n---\n") + comments = comments[1:] + params = [] + for comment in comments[:-1]: + if not comment.startswith("@lua"): + panic(path, line, f"Invalid function specification - got '{comment}'") + if comment.startswith("@lua@param"): + params.append(comment.split(" ", 2)[1]) + + out.write(f"---{comment.removeprefix('@lua')}\n") + + if not comments[-1].startswith("@exposed "): + panic(path, line, f"Invalid function exposure - got '{comments[-1]}'") + name = comments[-1].split(" ", 1)[1] + printmsg(path, line, f"function {name}") + lua_params = ", ".join(params) + out.write(f"function {name}({lua_params}) end\n\n") + + +def read_file(path: Path, out: TextIOWrapper): + print("Reading", path.relative_to(repo_root)) + with path.open("r") as f: + lines = f.read().splitlines() + + reader = Reader(lines) + while reader.has_next(): + doc_comment = reader.next_doc_comment() + if not doc_comment: + break + header_comment = None + if not doc_comment[0].startswith("@"): + if len(doc_comment) == 1: + continue + header_comment = doc_comment[0] + header = doc_comment[1:] + else: + header = doc_comment + + # include block + if header[0].startswith("@includefile "): + for comment in header: + if not comment.startswith("@includefile "): + panic( + path, + reader.line_no(), + f"Invalid include block - got line '{comment}'", + ) + filename = comment.split(" ", 1)[1] + out.write(f"-- Begin src/{filename}\n\n") + read_file(repo_root / "src" / filename, out) + out.write(f"-- End src/{filename}\n\n") continue - if line.startswith("/**"): - comment = True + # enum + if header[0].startswith("@exposeenum "): + if len(header) > 1: + panic( + path, + reader.line_no(), + f"Invalid enum exposure - one command expected, got {len(header)}", + ) + name = header[0].split(" ", 1)[1] + printmsg(path, reader.line_no(), f"enum {name}") + out.write(f"---@alias {name} integer\n") + if header_comment: + out.write(f"--- {header_comment}\n") + out.write("---@type { ") + out.write( + ", ".join( + [f"{variant}: {name}" for variant in reader.read_enum_variants()] + ) + ) + out.write(" }\n") + out.write(f"{name} = {{}}\n\n") continue - elif "*/" in line: - comment = False - ignore_this_comment = False - if not is_class: + # class + if header[0].startswith("@lua@class "): + name = header[0].split(" ", 1)[1] + classname = name.split(":")[0].strip() + printmsg(path, reader.line_no(), f"class {classname}") + + if header_comment: + out.write(f"--- {header_comment}\n") + out.write(f"---@class {name}\n") + # inline class + if len(header) > 1: + for field in header[1:]: + if not field.startswith("@lua@field "): + panic( + path, + reader.line_no(), + f"Invalid inline class exposure - all lines must be fields, got '{field}'", + ) + out.write(f"---{field.removeprefix('@lua')}\n") out.write("\n") - continue - if not comment: - continue - if ignore_this_comment: - continue - line = line.replace("*", "", 1).lstrip() - if line == "": - out.write("---\n") - elif line.startswith('@brief '): - # Doxygen comment, on a C++ only method - ignore_this_comment = True - elif line.startswith("@exposeenum "): - expose_next_enum_as = line.split(" ", 1)[1] - elif line.startswith("@exposed "): - exp = line.replace("@exposed ", "", 1) - params = ", ".join(last_params_names) - out.write(f"function {exp}({params}) end\n") - print(f"{loc} Wrote function {exp}(...)") - last_params_names = [] - elif line.startswith("@includefile "): - filename = line.replace("@includefile ", "", 1) - output.write(f"-- Now including data from src/{filename}.\n") - process_file(repo_root / 'src' / filename, output) - output.write(f'-- Back to {target.relative_to(repo_root)}.\n') - elif line.startswith("@lua"): - command = line.replace("@lua", "", 1) - if command.startswith("@param"): - last_params_names.append(command.split(" ", 2)[1]) - elif command.startswith("@class"): - print(f"{loc} Writing {command}") - if is_class: - out.write("\n") - is_class = True - elif not command.startswith("@field"): - is_class = False - - out.write("---" + command + "\n") - else: - if is_class: - is_class = False + continue + + # class definition + # save functions for later (print fields first) + funcs = [] + for comment in reader.read_class_body(): + if comment[-1].startswith("@exposed "): + funcs.append(comment) + continue + if len(comment) > 1 or not comment[0].startswith("@lua"): + continue + out.write(f"---{comment[0].removeprefix('@lua')}\n") + + if funcs: + # only define global if there are functions on the class + out.write(f"{classname} = {{}}\n\n") + else: out.write("\n") - # note the space difference from the branch above - out.write("--- " + line + "\n") + for func in funcs: + write_func(path, reader.line_no(), func, out) + continue + + # global function + if header[-1].startswith("@exposed "): + write_func(path, reader.line_no(), doc_comment, out) + continue -with lua_meta.open("w") as output: - output.write(BOILERPLATE[1:]) # skip the newline after triple quote - process_file(lua_api_file, output) +if __name__ == "__main__": + with lua_meta.open("w") as output: + output.write(BOILERPLATE[1:]) # skip the newline after triple quote + read_file(lua_api_file, output) diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 39df152169a..15be99c6fa8 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -106,7 +106,7 @@ int c2_register_callback(lua_State *L); /** * Writes a message to the Chatterino log. * - * @lua@param level LogLevel The desired level. + * @lua@param level c2.LogLevel The desired level. * @lua@param ... any Values to log. Should be convertible to a string with `tostring()`. * @exposed c2.log */ diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 29f5173d28e..abc6b421f6f 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -21,7 +21,7 @@ enum class LPlatform { }; /** - * @lua@class Channel: IWeakResource + * @lua@class Channel */ struct ChannelRef { static void createMetatable(lua_State *L); @@ -100,7 +100,7 @@ struct ChannelRef { * Compares the channel Type. Note that enum values aren't guaranteed, just * that they are equal to the exposed enum. * - * @lua@return bool + * @lua@return boolean * @exposed Channel:is_twitch_channel */ static int is_twitch_channel(lua_State *L); @@ -193,7 +193,7 @@ struct ChannelRef { /** * Finds a channel by the Twitch user ID of its owner. * - * @lua@param string id ID of the owner of the channel. + * @lua@param id string ID of the owner of the channel. * @lua@return Channel? * @exposed Channel.by_twitch_id */ @@ -216,13 +216,12 @@ struct LuaRoomModes { bool subscriber_only = false; /** - * @lua@field emotes_only boolean Whether or not text is allowed in messages. - * Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes */ bool emotes_only = false; /** - * @lua@field unique_chat number? Time in minutes you need to follow to chat or nil. + * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. */ std::optional follower_only; /** From b991b957f0cc5cd491161bf96191acc5e03ebae5 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 31 Mar 2024 11:46:58 +0200 Subject: [PATCH 27/33] fix: missing rerender on clear (#5282) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c49ca3401..af86fe2737d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) +- Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 2795e5ffe7b..c76373d5679 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -782,6 +782,7 @@ void ChannelView::clearMessages() this->scrollBar_->setMaximum(0); this->scrollBar_->setMinimum(0); this->queueLayout(); + this->update(); this->lastMessageHasAlternateBackground_ = false; this->lastMessageHasAlternateBackgroundReverse_ = true; From 694d53ad20d853e0d85e7c6a0a4ed57c9ee7e7ef Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 31 Mar 2024 13:07:43 +0200 Subject: [PATCH 28/33] Fix some documentations & comments (#5286) * add comments for the new reward filters * slightly improve documentation of r9k values --- src/controllers/filters/lang/Filter.cpp | 3 +++ src/widgets/settingspages/GeneralPage.cpp | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index ef0cfd15ce2..9c3ecb0228b 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -50,6 +50,9 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) * message.content * message.length * + * reward.title + * reward.cost + * reward.id */ using MessageFlag = chatterino::MessageFlag; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 74746be8ad1..d129e46eb71 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -462,7 +462,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addTitle("Messages"); layout.addCheckbox( "Separate with lines", s.separateMessages, false, - "Adds a line inbetween each message to help better tell them apart."); + "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."); @@ -904,7 +904,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) toggleLocalr9kShortcut + "."); layout.addCheckbox("Hide similar messages", s.similarityEnabled); //layout.addCheckbox("Gray out matches", s.colorSimilarDisabled); - layout.addCheckbox("By the same user", s.hideSimilarBySameUser); + layout.addCheckbox( + "By the same user", s.hideSimilarBySameUser, false, + "When checked, messages that are very similar to each other can still " + "be shown as long as they're sent by different users."); layout.addCheckbox("Hide my own messages", s.hideSimilarMyself); layout.addCheckbox("Receive notification sounds from hidden messages", s.shownSimilarTriggerHighlights); @@ -920,7 +923,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToFloat(args.value, 0.9f); - }); + }, + true, + "A value of 0.9 means the messages need to be 90% similar to be marked " + "as similar."); layout.addDropdown( "Maximum delay between messages", {"5s", "10s", "15s", "30s", "60s", "120s"}, s.hideSimilarMaxDelay, @@ -929,7 +935,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToInt(args.value, 5); - }); + }, + true, + "A value of 5s means if there's a 5s break between messages, we will " + "stop looking further through the messages for similarities."); layout.addDropdown( "Amount of previous messages to check", {"1", "2", "3", "4", "5"}, s.hideSimilarMaxMessagesToCheck, From 905aa4e923006dc989d8febbebf1da1cc512a5d5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 1 Apr 2024 00:04:11 +0200 Subject: [PATCH 29/33] refactor: About page (#5287) --- CHANGELOG.md | 1 + resources/avatars/anon.png | Bin 0 -> 1612 bytes resources/contributors.txt | 141 ++++++------- src/CMakeLists.txt | 3 + src/widgets/layout/FlowLayout.cpp | 252 ++++++++++++++++++++++++ src/widgets/layout/FlowLayout.hpp | 104 ++++++++++ src/widgets/settingspages/AboutPage.cpp | 60 ++++-- 7 files changed, 474 insertions(+), 87 deletions(-) create mode 100644 resources/avatars/anon.png create mode 100644 src/widgets/layout/FlowLayout.cpp create mode 100644 src/widgets/layout/FlowLayout.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index af86fe2737d..c41fd957019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) +- Minor: Changed the layout of the about page. (#5287) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/resources/avatars/anon.png b/resources/avatars/anon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7993edcbbbed49dc84078ac0ba2c44a3c7d2d91 GIT binary patch literal 1612 zcmV-S2DABzP)C00033P)t-s00030 z|NsC0|L5oD)YR0lu&~U`%-r1EudlDWySu2UsLRXCs;a8Mz`&ZCn#IM%kB^U_prC$! ze!#%Mn3$NNqN2sc#gmhhwY9Z(cXyeYnZLikj*gC-o1444yN{2LudlD0o14A8y>4!9 ziHV7voSch`i?+75iHV7CW*rKF^!p`oFho12xDm64H=jg5_pii(Ga zhk}BFfq{X5fPjC0e|~;`dwY9&dU|$tc5`!cZfH9JB{OjlQ2V{3ADfP;vUl9`{Us+kUJ^Yixh`Tzg_|NsC0|NsB~{wJ}e5&!@Mwn;=mRCwC$np;=eOcaLqB;kxGf=5&+ zBBCgMia`A|0XZfha{m8+r8CK7jT*oTV_aQpo~vH8ect`AD5%FPC>2_gr;{O%X9X)a*Z`9rWSGF`w*{tG{>%_eyPbN%E+)M_>I2fp(=3vez# zt2XK%(J=hKh!rKR@y=Toae*o4;u?t}T zz4|<`?Bj!=%$ka1K18_j!*>**)rZ)QnVk9&yaff%G*0_xp!CNJ7!W=2ZzK4Kw4?Ir zpW(ZISAf=VK3#eK85g`uCNce#p2^@eH zFvkAzcr+Xi2mg7%EzKQ+wEn*HGI|y)Mr-zc>FFV z11{bGIRUkA7l8eS(Y=(wEfgF8zkxCJcd;K+@Pb=Nc@m&GW%HW^!8OkD38;hd2p<6R z8%N+8DRP4&wg8Kq1a7g91JszGqA%Y8&v=_SK-~<0U!Y;5Ct(Tl8OIy(l~k=|7QpIR`+*x5s@fe4i2q%HtOAShU)LMgVc_x7r@ZWyaWE&`m`P}RZf zoB*9eX}?bXL%A-yKI41O()bn(Y&R+w>bo zG05i($w$y|pj#1I&0mK*J3EJ@ds}b%3734fkYpQhY@v;5wVEa^VyEL%3S2q}mZ{&l z1sZ|T!RR;u*90iZ0ywrnU~*f}xa5(*b7^3fX0YW{&*1YG;>JRB0#*|}v-9o%WsN21 zHpYC40a%+oV+}GgtWCJQlA7Pgz*AV+UkC#S{N?ybT0h@MP>ciG%Icr%vk?Rw3ETcHL=GbQ*+pa`GVsJp zg&Dh5DUvKMv=@)U2)LqT%~o?!2^hY;Y +#include +#include +#include + +namespace { + +using namespace chatterino; + +class Linebreak : public QWidget +{ +}; + +} // namespace + +namespace chatterino { + +FlowLayout::FlowLayout(QWidget *parent, Options options) + : QLayout(parent) + , hSpace_(options.hSpacing) + , vSpace_(options.vSpacing) +{ + if (options.margin >= 0) + { + this->setContentsMargins(options.margin, options.margin, options.margin, + options.margin); + } +} + +FlowLayout::FlowLayout(Options options) + : FlowLayout(nullptr, options) +{ +} + +FlowLayout::~FlowLayout() +{ + for (auto *item : this->itemList_) + { + delete item; + } + this->itemList_ = {}; +} + +void FlowLayout::addItem(QLayoutItem *item) +{ + this->itemList_.push_back(item); +} + +void FlowLayout::addLinebreak(int height) +{ + auto *linebreak = new Linebreak; + linebreak->setFixedHeight(height); + this->addWidget(linebreak); +} + +int FlowLayout::horizontalSpacing() const +{ + if (this->hSpace_ >= 0) + { + return this->hSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutHorizontalSpacing); +} + +void FlowLayout::setHorizontalSpacing(int value) +{ + if (this->hSpace_ == value) + { + return; + } + this->hSpace_ = value; + this->invalidate(); +} + +int FlowLayout::verticalSpacing() const +{ + if (this->vSpace_ >= 0) + { + return this->vSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutVerticalSpacing); +} + +void FlowLayout::setVerticalSpacing(int value) +{ + if (this->vSpace_ == value) + { + return; + } + this->vSpace_ = value; + this->invalidate(); +} + +int FlowLayout::count() const +{ + return static_cast(this->itemList_.size()); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + return this->itemList_[static_cast(index)]; + } + return nullptr; +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + auto *it = this->itemList_[static_cast(index)]; + this->itemList_.erase(this->itemList_.cbegin() + + static_cast(index)); + return it; + } + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return {}; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + return this->doLayout({0, 0, width, 0}, true); +} + +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + this->doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return this->minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const auto *item : this->itemList_) + { + size = size.expandedTo(item->minimumSize()); + } + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), + margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const +{ + auto margins = this->contentsMargins(); + QRect effectiveRect = rect.adjusted(margins.left(), margins.top(), + -margins.right(), -margins.bottom()); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + for (QLayoutItem *item : this->itemList_) + { + auto *linebreak = dynamic_cast(item->widget()); + if (linebreak) + { + item->setGeometry({x, y, 0, linebreak->height()}); + x = effectiveRect.x(); + y = y + lineHeight + linebreak->height(); + lineHeight = 0; + continue; + } + + auto space = this->getSpacing(item); + int nextX = x + item->sizeHint().width() + space.width(); + if (nextX - space.width() > effectiveRect.right() && lineHeight > 0) + { + x = effectiveRect.x(); + y = y + lineHeight + space.height(); + nextX = x + item->sizeHint().width() + space.width(); + lineHeight = 0; + } + + if (!testOnly) + { + item->setGeometry({QPoint{x, y}, item->sizeHint()}); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + + return y + lineHeight - rect.y() + margins.bottom(); +} + +int FlowLayout::defaultSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) + { + return -1; + } + if (auto *widget = dynamic_cast(parent)) + { + return widget->style()->pixelMetric(pm, nullptr, widget); + } + if (auto *layout = dynamic_cast(parent)) + { + return layout->spacing(); + } + return -1; +} + +QSize FlowLayout::getSpacing(QLayoutItem *item) const +{ + // called if there isn't any parent or the parent can't provide any spacing + auto fallbackSpacing = [&](auto dir) { + if (auto *widget = item->widget()) + { + return widget->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, dir); + } + if (auto *layout = item->layout()) + { + return layout->spacing(); + } + return 0; + }; + + QSize spacing(this->horizontalSpacing(), this->verticalSpacing()); + if (spacing.width() == -1) + { + spacing.rwidth() = fallbackSpacing(Qt::Horizontal); + } + if (spacing.height() == -1) + { + spacing.rheight() = fallbackSpacing(Qt::Vertical); + } + return spacing; +} + +} // namespace chatterino diff --git a/src/widgets/layout/FlowLayout.hpp b/src/widgets/layout/FlowLayout.hpp new file mode 100644 index 00000000000..39a359ff143 --- /dev/null +++ b/src/widgets/layout/FlowLayout.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +#include + +namespace chatterino { + +/// @brief A QLayout wrapping items +/// +/// Similar to a box layout that wraps its items. It's not super optimized. +/// Some computations in #doLayout() could be cached. +/// +/// This is based on the Qt flow layout example: +/// https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html +class FlowLayout : public QLayout +{ +public: + struct Options { + int margin = -1; + int hSpacing = -1; + int vSpacing = -1; + }; + + explicit FlowLayout(QWidget *parent, Options options = {-1, -1, -1}); + explicit FlowLayout(Options options = {-1, -1, -1}); + + ~FlowLayout() override; + FlowLayout(const FlowLayout &) = delete; + FlowLayout(FlowLayout &&) = delete; + FlowLayout &operator=(const FlowLayout &) = delete; + FlowLayout &operator=(FlowLayout &&) = delete; + + /// @brief Adds @a item to this layout + /// + /// Ownership of @a item is transferred. This method isn't usually called + /// in application code (use addWidget/addLayout). + /// See QLayout::addItem for more information. + void addItem(QLayoutItem *item) override; + + /// @brief Adds a linebreak to this layout + /// + /// @param height Specifies the height of the linebreak + void addLinebreak(int height = 0); + + /// @brief Spacing on the horizontal axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int horizontalSpacing() const; + + /// Setter for #horizontalSpacing(). -1 to use defaults. + void setHorizontalSpacing(int value); + + /// @brief Spacing on the vertical axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int verticalSpacing() const; + + /// Setter for #verticalSpacing(). -1 to use defaults. + void setVerticalSpacing(int value); + + /// From QLayout. This layout doesn't expand in any direction. + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; + + QSize minimumSize() const override; + QSize sizeHint() const override; + + void setGeometry(const QRect &rect) override; + + int count() const override; + QLayoutItem *itemAt(int index) const override; + + /// From QLayout. Ownership is transferred to the caller + QLayoutItem *takeAt(int index) override; + +private: + /// @brief Computes the layout + /// + /// @param rect The area in which items can be layed out + /// @param testOnly If set, items won't be moved, only the total height + /// will be computed. + /// @returns The total height including margins. + int doLayout(const QRect &rect, bool testOnly) const; + + /// @brief Computes the default spacing based for items on the parent + /// + /// @param pm Either PM_LayoutHorizontalSpacing or PM_LayoutVerticalSpacing + /// for the respective direction. + /// @returns The spacing in dp, -1 if there isn't any parent + int defaultSpacing(QStyle::PixelMetric pm) const; + + /// Computes the spacing for @a item + QSize getSpacing(QLayoutItem *item) const; + + std::vector itemList_; + int hSpace_ = -1; + int vSpace_ = -1; + int lineSpacing_ = -1; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 78597c5fca3..89c985c5e8d 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -8,6 +8,7 @@ #include "util/RemoveScrollAreaBackground.hpp" #include "widgets/BasePopup.hpp" #include "widgets/helper/SignalLabel.hpp" +#include "widgets/layout/FlowLayout.hpp" #include #include @@ -54,6 +55,7 @@ AboutPage::AboutPage() auto label = vbox.emplace(version.buildString() + "
" + version.runningString()); + label->setWordWrap(true); label->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextBrowserInteraction); } @@ -137,15 +139,15 @@ AboutPage::AboutPage() l.emplace("Facebook emojis provided by Facebook")->setOpenExternalLinks(true); l.emplace("Apple emojis provided by Apple")->setOpenExternalLinks(true); l.emplace("Google emojis provided by Google")->setOpenExternalLinks(true); - l.emplace("Emoji datasource provided by Cal Henderson" + l.emplace("Emoji datasource provided by Cal Henderson " "(show license)")->setOpenExternalLinks(true); // clang-format on } // Contributors - auto contributors = layout.emplace("Contributors"); + auto contributors = layout.emplace("People"); { - auto l = contributors.emplace(); + auto l = contributors.emplace(); QFile contributorsFile(":/contributors.txt"); contributorsFile.open(QFile::ReadOnly); @@ -166,11 +168,24 @@ AboutPage::AboutPage() continue; } + if (line.startsWith(u"@header")) + { + if (l->count() != 0) + { + l->addLinebreak(20); + } + auto *label = new QLabel(QStringLiteral("

%1

") + .arg(line.mid(8).trimmed())); + l->addWidget(label); + l->addLinebreak(8); + continue; + } + QStringList contributorParts = line.split("|"); - if (contributorParts.size() != 4) + if (contributorParts.size() != 3) { - qCDebug(chatterinoWidget) + qCWarning(chatterinoWidget) << "Missing parts in line" << line; continue; } @@ -178,39 +193,42 @@ AboutPage::AboutPage() QString username = contributorParts[0].trimmed(); QString url = contributorParts[1].trimmed(); QString avatarUrl = contributorParts[2].trimmed(); - QString role = contributorParts[3].trimmed(); auto *usernameLabel = new QLabel("" + username + ""); usernameLabel->setOpenExternalLinks(true); - auto *roleLabel = new QLabel(role); + usernameLabel->setToolTip(url); - auto contributorBox2 = l.emplace(); + auto contributorBox2 = l.emplace(); - const auto addAvatar = [&avatarUrl, &contributorBox2] { - if (!avatarUrl.isEmpty()) + const auto addAvatar = [&] { + auto *avatar = new QLabel(); + QPixmap avatarPixmap; + if (avatarUrl.isEmpty()) + { + // TODO: or anon.png + avatarPixmap.load(":/avatars/anon.png"); + } + else { - QPixmap avatarPixmap; avatarPixmap.load(avatarUrl); - - auto avatar = contributorBox2.emplace(); - avatar->setPixmap(avatarPixmap); - avatar->setFixedSize(64, 64); - avatar->setScaledContents(true); } + + avatar->setPixmap(avatarPixmap); + avatar->setFixedSize(64, 64); + avatar->setScaledContents(true); + contributorBox2->addWidget(avatar, 0, Qt::AlignCenter); }; - const auto addLabels = [&contributorBox2, &usernameLabel, - &roleLabel] { + const auto addLabels = [&] { auto *labelBox = new QVBoxLayout(); contributorBox2->addLayout(labelBox); - labelBox->addWidget(usernameLabel); - labelBox->addWidget(roleLabel); + labelBox->addWidget(usernameLabel, 0, Qt::AlignCenter); }; - addLabels(); addAvatar(); + addLabels(); } } } From 2a447d3c950cf683414d4fa716846de499e1e68c Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 1 Apr 2024 20:51:12 +0200 Subject: [PATCH 30/33] fix: use 3x scale factor for base size multiplier (#5291) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchEmotes.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c41fd957019..29850b5c3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,7 +106,7 @@ - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) -- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279, #5291) - Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 918d504a46e..4baa13f2027 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -439,13 +439,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { auto baseSize = getEmoteExpectedBaseSize(id); + auto emote3xScaleFactor = getEmote3xScaleFactor(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), - Image::fromUrl(getEmoteLink(id, "3.0"), - getEmote3xScaleFactor(id), baseSize * 4), + Image::fromUrl(getEmoteLink(id, "3.0"), emote3xScaleFactor, + baseSize * (1.0 / emote3xScaleFactor)), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); From 92e75784fce0fe5ae0ce892cfb96863432875a1a Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 2 Apr 2024 03:50:53 -0700 Subject: [PATCH 31/33] feat: report duration for multi-month anon sub gifts (#5293) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 42 ++++++++++++++++++++++ src/util/SampleData.cpp | 6 ++++ 3 files changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29850b5c3c3..4a1431df1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Minor: Changed the layout of the about page. (#5287) +- Minor: Add duration to multi-month anon sub gift messages. (#5293) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 8a9cc9e6c2c..afda22592f2 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -53,6 +53,8 @@ const QSet SPECIAL_MESSAGE_TYPES{ "viewermilestone", // watch streak, but other categories possible in future }; +const QString ANONYMOUS_GIFTER_ID = "274598607"; + MessagePtr generateBannedMessage(bool confirmedBan) { const auto linkColor = MessageColor(MessageColor::Link); @@ -516,6 +518,26 @@ std::vector parseUserNoticeMessage(Channel *channel, { messageText = "Announcement"; } + else if (msgType == "subgift" && + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + messageText = + QString("An anonymous user gifted %1 months of a Tier " + "%2 sub to %3!") + .arg(QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); @@ -1010,6 +1032,26 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } + else if (msgType == "subgift" && + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + messageText = + QString("An anonymous user gifted %1 months of a Tier " + "%2 sub to %3!") + .arg(QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 953646139bd..2c5b7ca4f8e 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -70,6 +70,12 @@ const QStringList &getSampleSubMessages() // hyperbolicxd gifted a sub to quote_if_nam R"(@badges=subscriber/0,premium/1;color=#00FF7F;display-name=hyperbolicxd;emotes=;id=b20ef4fe-cba8-41d0-a371-6327651dc9cc;login=hyperbolicxd;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=quote_if_nam;msg-param-recipient-id=217259245;msg-param-recipient-user-name=quote_if_nam;msg-param-sender-count=1;msg-param-sub-plan-name=Channel\sSubscription\s(nymn_hs);msg-param-sub-plan=1000;room-id=62300805;subscriber=1;system-msg=hyperbolicxd\sgifted\sa\sTier\s1\ssub\sto\squote_if_nam!\sThis\sis\stheir\sfirst\sGift\sSub\sin\sthe\schannel!;tmi-sent-ts=1528190938558;turbo=0;user-id=111534250;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", + // multi-month sub gift + R"(@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\sSubscription\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\sgifted\s6\smonths\sof\sTier\s1\sto\skimmi_tm.\sThey've\sgifted\s334\smonths\sin\sthe\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #mxddy)", + + // multi-month anon sub gift + R"(@msg-param-goal-user-contributions=1;system-msg=An\sanonymous\suser\sgifted\sa\sTier\s1\ssub\sto\sMohammadrezaDH!\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\slee\sgoal\s:-) :tmi.twitch.tv USERNOTICE #jmarianne)", + // first time sub R"(@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\sjust\ssubscribed\swith\sTwitch\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", From 8db0bb464da134f8032583d1043300159a40edee Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 2 Apr 2024 21:59:59 +0200 Subject: [PATCH 32/33] fix: use login name when parsing highlights (#5295) --- CHANGELOG.md | 1 + src/messages/SharedMessageBuilder.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1431df1a4..34c63cdb4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) - Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) +- Bugfix: Fixed highlights triggering for ignored users in announcements. (#5295) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index 87a3ae9b458..98ec30473f4 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -150,7 +150,7 @@ void SharedMessageBuilder::parseUsername() void SharedMessageBuilder::parseHighlights() { - if (getSettings()->isBlacklistedUser(this->ircMessage->nick())) + if (getSettings()->isBlacklistedUser(this->message().loginName)) { // Do nothing. We ignore highlights from this user. return; @@ -158,7 +158,7 @@ void SharedMessageBuilder::parseHighlights() auto badges = SharedMessageBuilder::parseBadgeTag(this->tags); auto [highlighted, highlightResult] = getIApp()->getHighlights()->check( - this->args, badges, this->ircMessage->nick(), this->originalMessage_, + this->args, badges, this->message().loginName, this->originalMessage_, this->message().flags); if (!highlighted) From 2ea24c1a9dfdd9a2496b4636ee8757eb76a66d6a Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 3 Apr 2024 21:08:52 +0200 Subject: [PATCH 33/33] fix: use `deleteLater` for network objects and order them (#5297) --- CHANGELOG.md | 2 +- src/common/network/NetworkManager.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c63cdb4a2..88793ee3b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,7 +201,7 @@ - Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) -- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) +- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254, #5297) - Dev: `clang-tidy` CI now uses Qt 6. (#5273) - Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) diff --git a/src/common/network/NetworkManager.cpp b/src/common/network/NetworkManager.cpp index 956c2e79f31..eb1b7ec5229 100644 --- a/src/common/network/NetworkManager.cpp +++ b/src/common/network/NetworkManager.cpp @@ -24,15 +24,19 @@ void NetworkManager::deinit() assert(NetworkManager::workerThread); assert(NetworkManager::accessManager); + // delete the access manager first: + // - put the event on the worker thread + // - wait for it to process + NetworkManager::accessManager->deleteLater(); + NetworkManager::accessManager = nullptr; + if (NetworkManager::workerThread) { NetworkManager::workerThread->quit(); NetworkManager::workerThread->wait(); } - delete NetworkManager::accessManager; - NetworkManager::accessManager = nullptr; - delete NetworkManager::workerThread; + NetworkManager::workerThread->deleteLater(); NetworkManager::workerThread = nullptr; }