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/71] 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 90c9d449b..049896845 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 35cd4be01..a5554570a 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 566c79fe1..d14d1f914 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 3904d1a20..898ea0e32 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/71] 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 049896845..6462cc960 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 0cac39136..3a38cadf0 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 50156c5e1..e6cb7597e 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -139,15 +139,32 @@ public: 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 @@ private: 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/71] 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 6462cc960..ce88f1f8f 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 086f59495..cf2fad9bd 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 291e95b22..497c25260 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 c37cfb7ef..df042b24f 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 6866c2cc0..9361cd1ff 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 f610ae25d..4c78d6edc 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 562b6c07b..fc0a255cc 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 87683ce0e..0f23df343 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 8ae91cd97..986fbbac3 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 7eeffaf71..f6a58a0bb 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/71] 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 ce88f1f8f..9a2930dd6 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 ba5e85b4d..24853e752 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 b549c9e9b..cbc04c03b 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 000000000..27ace07a9 --- /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 000000000..c71d889db --- /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/71] 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 9a2930dd6..19dcbe7a7 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 bf31dbb00..620ca452d 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -165,12 +165,22 @@ public: 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 a1aba0575..9dc53b9bf 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++) { - return combinePath(qgetenv("appdata"), - "Chatterino\\Custom\\Commands.txt"); + QString commandTrigger = + view->getModel()->index(i, 0).data().toString(); + commands[commandTrigger].push_back(i); } + + for (const auto &[commandTrigger, rowIndices] : commands) + { + 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 ea97440bd..d88c00a61 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/71] 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 19dcbe7a7..7cac0b0f8 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 e895f7630..56d1e2ed3 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 efc1d1b56..126bb40c9 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 8a177227f..f54f57d2a 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -70,7 +70,21 @@ public: * * 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 17b9b795d..29d70e0a1 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 be765da85..ed3c1a7a6 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 @@ private: 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 ffa949d7f..31b7d4fe5 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 bbb45302f..de68a43f7 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -71,6 +71,9 @@ public: const QString &getText() const; FlagsEnum getFlags() const; + int getWordId() const; + void setWordId(int wordId); + protected: bool trailingSpace = true; @@ -83,6 +86,13 @@ private: * 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 3a38cadf0..3cf64e116 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 53ffd5e1a..f1f80cf0e 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 000000000..9ce0c7f21 --- /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/71] 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 44a8015c3..3b24a9978 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/71] 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 7cac0b0f8..56fd13093 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 24853e752..ba5e85b4d 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 cbc04c03b..b549c9e9b 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 27ace07a9..000000000 --- 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 c71d889db..000000000 --- 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/71] 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 111a91296..0f072a364 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 ad1523523..e22b264f5 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/71] 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 56fd13093..9496106cc 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 dfc9fe0a0..956c2e79f 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 530aaae1f..b02ce04e5 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 adf46b6f7..51842dd57 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 7590c8a46..256743a4f 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 7029488af..2f6b8102f 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/71] 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 9496106cc..865a41481 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 be3ebb8ff..fe7e5ed65 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 @@ public: 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 @@ public: 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 51842dd57..ed81dd9e6 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 1e169a927..434d9f66d 100644 --- a/src/common/network/NetworkPrivate.hpp +++ b/src/common/network/NetworkPrivate.hpp @@ -60,7 +60,7 @@ public: 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 d14d1f914..a86746195 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 497c25260..d70be6489 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 fc0a255cc..4609fee7c 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 6dfb3b20e..4450b2a01 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -107,14 +107,14 @@ public: 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 d806db4bd..09204f93d 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 2b8c0ec27..82234a99c 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 03fbdac97..b4fd31044 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 91d330c5e..2a1a46a94 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 2a3b9a14e..daf17021f 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 8c0838f6b..697db1e32 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 7bc4a2f5f..4b32786e9 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 8907a2d2e..244e2be98 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 8134178c5..2cc36ca98 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 2a7fd6f50..cac4e02fd 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 d0b59d0c6..2001b8ccb 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 000000000..0325102e3 --- /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 3cf64e116..1454b4999 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 50136dcee..74746be8a 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 d2a0a27e1..7e1625a70 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -276,7 +276,7 @@ public: 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 @@ public: for (const auto &item : items) { - combo->addItem(QString::fromStdString(std::string(item))); + combo->addItem(item.toString()); } if (!defaultValueText.isEmpty()) @@ -296,8 +296,7 @@ public: 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 f1f80cf0e..fb5730048 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 000000000..80c265efe --- /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/71] 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 865a41481..b74cadd26 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 cb7311275..7ee4fa588 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/71] 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 67ae8fe79..b901e8e6d 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/71] 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 b74cadd26..1c039db91 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 af266889c..88c0f671e 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/71] 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 1c039db91..5245db53b 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 af5dadcf7..cc71558f6 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/71] 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 0f072a364..b522bba5d 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 e22b264f5..2f9b6b3d9 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/71] 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 5245db53b..a8182382c 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/71] 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 a8182382c..cd6df89d2 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 b0315a8aa..35b8efb1c 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 8b3cea430..7106f872f 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 49cdf8e35..62d459e22 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 e227223de..6c32e0ccb 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 97a1ad51d..0c60688b2 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 471b6d03b..78597c5fc 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/71] 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 cd6df89d2..37080a27b 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 9a71c89ac..f591e8f31 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/71] 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 000000000..4884285eb --- /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 b522bba5d..cf47eacaf 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 2f9b6b3d9..6c39a93a7 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 37080a27b..b8287d63a 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/71] 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 b8287d63a..1ce0f4f60 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 1454b4999..9c45c6d9a 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/71] 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 1ce0f4f60..b82b51b8a 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 7ae61991a..ef0cfd15c 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 c8afbd769..01d7a765e 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 2fbc5fd95..6ca9d373c 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 b9e0b2321..bdbe120dd 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 88c0f671e..524d0375d 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 9c45c6d9a..2795e5ffe 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/71] 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 b82b51b8a..b3b0c0500 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 4c87e472b..918d504a4 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 d793ce723..17e50b11f 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -52,7 +52,6 @@ public: 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/71] 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 0feaad9dc..cfbe49d31 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 b3b0c0500..d89ebd1ee 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/71] 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 d89ebd1ee..f9ec75070 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 9bf6f57c0..95d2282be 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 1309d7bab..32eda387f 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 df042b24f..39df15216 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 9361cd1ff..64af18c01 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 4c78d6edc..5443a751f 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 4450b2a01..2adbe9067 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -98,8 +98,8 @@ public: // 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 @@ public: // 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 0f23df343..8c2d80556 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/71] refactor: improve LuaLS generator (#5283) --- CHANGELOG.md | 2 +- docs/plugin-meta.lua | 71 ++-- scripts/make_luals_meta.py | 393 ++++++++++++++------- src/controllers/plugins/LuaAPI.hpp | 2 +- src/controllers/plugins/api/ChannelRef.hpp | 11 +- 5 files changed, 311 insertions(+), 168 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ec75070..01c49ca34 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 2cc56af59..7b72b46d5 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 22240da19..58a062428 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -12,25 +12,26 @@ It assumes comments look like: - 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 @@ BOILERPLATE = """ 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 @@ lua_meta = repo_root / "docs" / "plugin-meta.lua" 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: +def strip_line(line: str): + return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() + + +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 + + 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 + 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() - # 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 + 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 - 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 - 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 - continue - - if line.startswith("/**"): - comment = True - continue - elif "*/" in line: - comment = False - ignore_this_comment = False - - if not is_class: - 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") + header_comment = doc_comment[0] + header = doc_comment[1:] else: - if is_class: - is_class = False + 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 + + # 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 + + # 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 + + # 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 39df15216..15be99c6f 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 29f5173d2..abc6b421f 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 @@ public: * 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 @@ public: /** * 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/71] 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 01c49ca34..af86fe273 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 2795e5ffe..c76373d56 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/71] 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 ef0cfd15c..9c3ecb022 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 74746be8a..d129e46eb 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/71] 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 | 62 +++--- 7 files changed, 475 insertions(+), 88 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 af86fe273..c41fd9570 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 000000000..39a359ff1 --- /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 78597c5fc..89c985c5e 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()) { - QPixmap avatarPixmap; - avatarPixmap.load(avatarUrl); - - auto avatar = contributorBox2.emplace(); - avatar->setPixmap(avatarPixmap); - avatar->setFixedSize(64, 64); - avatar->setScaledContents(true); + // TODO: or anon.png + avatarPixmap.load(":/avatars/anon.png"); } + else + { + avatarPixmap.load(avatarUrl); + } + + 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/71] 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 c41fd9570..29850b5c3 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 918d504a4..4baa13f20 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/71] 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 29850b5c3..4a1431df1 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 8a9cc9e6c..afda22592 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 953646139..2c5b7ca4f 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/71] 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 4a1431df1..34c63cdb4 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 87a3ae9b4..98ec30473 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/71] 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 34c63cdb4..88793ee3b 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 956c2e79f..eb1b7ec52 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; } From 25a69fd10e8ada6015d8b937f6c82ce220c742e4 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 7 Apr 2024 12:03:14 +0200 Subject: [PATCH 34/71] Release v2.5.0-beta.1 (#5303) --- .CI/chatterino-installer.iss | 2 +- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 +- docs/make-release.md | 4 +++- resources/com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss index 2e3edbf52..fddd668f9 100644 --- a/.CI/chatterino-installer.iss +++ b/.CI/chatterino-installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Chatterino" -#define MyAppVersion "2.4.6" +#define MyAppVersion "2.5.0" #define MyAppPublisher "Chatterino Team" #define MyAppURL "https://www.chatterino.com" #define MyAppExeName "chatterino.exe" diff --git a/CHANGELOG.md b/CHANGELOG.md index 88793ee3b..e9ca57c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.5.0-beta.1 + - 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) diff --git a/CMakeLists.txt b/CMakeLists.txt index 14efcb0da..3a08cf79e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ if(BUILD_BENCHMARKS) endif() project(chatterino - VERSION 2.4.6 + VERSION 2.5.0 DESCRIPTION "Chat client for twitch.tv" HOMEPAGE_URL "https://chatterino.com/" ) diff --git a/docs/make-release.md b/docs/make-release.md index c28dead6b..1509289fd 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -8,7 +8,9 @@ - [ ] Add a new release at the top of the `releases` key in `resources/com.chatterino.chatterino.appdata.xml` This cannot use dash to denote a pre-release identifier, you have to use a tilde instead. -- [ ] Updated version code in `.CI/chatterino-installer.iss` +- [ ] Updated version code in `.CI/chatterino-installer.iss` + This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0` + - [ ] Update the changelog `## Unreleased` section to the new version `CHANGELOG.md` Make sure to leave the `## Unreleased` line unchanged for easier merges diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index ce9e25db8..5f76c9f6d 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1 + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.6 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 5d978b19a..3673e5b23 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.4.6" +#define CHATTERINO_VERSION "2.5.0-beta.1" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From dd62707d535cd556f89ee3080ac234cf2ba5c426 Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 9 Apr 2024 14:25:08 +0200 Subject: [PATCH 35/71] fix: hide tooltip on window leave event (#5309) --- CHANGELOG.md | 2 ++ src/widgets/BaseWindow.cpp | 1 + src/widgets/BaseWindow.hpp | 1 + src/widgets/splits/SplitHeader.cpp | 14 ++++++++++++++ 4 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ca57c90..10c71cf5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) + ## 2.5.0-beta.1 - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index c819c8f4a..a61322fce 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -527,6 +527,7 @@ void BaseWindow::changeEvent(QEvent *) void BaseWindow::leaveEvent(QEvent *) { + this->leaving.invoke(); } void BaseWindow::moveTo(QPoint point, widgets::BoundsChecking mode) diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index e59c8ae81..b9f21b08d 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -85,6 +85,7 @@ public: void setTopMost(bool topMost); pajlada::Signals::NoArgSignal closing; + pajlada::Signals::NoArgSignal leaving; static bool supportsCustomWindowFrame(); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index b8b811ca4..ed4a59b18 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -257,6 +257,20 @@ SplitHeader::SplitHeader(Split *split) getSettings()->headerStreamTitle.connect(_, this->managedConnections_); getSettings()->headerGame.connect(_, this->managedConnections_); getSettings()->headerUptime.connect(_, this->managedConnections_); + + auto *window = dynamic_cast(this->window()); + if (window) + { + // Hack: In some cases Qt doesn't send the leaveEvent the "actual" last mouse receiver. + // This can happen when quickly moving the mouse out of the window and right clicking. + // To prevent the tooltip from getting stuck, we use the window's leaveEvent. + this->managedConnections_.managedConnect(window->leaving, [this] { + if (this->tooltipWidget_->isVisible()) + { + this->tooltipWidget_->hide(); + } + }); + } } void SplitHeader::initializeLayout() From 116e82dcc508fb3b8a7e7f3c27953898b00e31fb Mon Sep 17 00:00:00 2001 From: nealxm <88364802+nealxm@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:33:58 -0700 Subject: [PATCH 36/71] fix: the version string not showing up as expected in Finder on macOS (#5311) --- CHANGELOG.md | 1 + cmake/MacOSXBundleInfo.plist.in | 2 -- src/CMakeLists.txt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c71cf5f..990cca03d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) +- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) ## 2.5.0-beta.1 diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index ae08eb0d9..9077068db 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -6,8 +6,6 @@ English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa545a644..eb64bcf31 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -986,7 +986,6 @@ if (APPLE AND BUILD_APP) PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Chatterino" MACOSX_BUNDLE_GUI_IDENTIFIER "com.chatterino" - MACOSX_BUNDLE_INFO_STRING "Chat client for Twitch" MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" From 326f71aef8e663d307838ffe2b127c4df454088b Mon Sep 17 00:00:00 2001 From: nealxm <88364802+nealxm@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:01:46 -0700 Subject: [PATCH 37/71] add nealxm to contributors list (#5312) --- resources/avatars/nealxm.png | Bin 0 -> 2689 bytes resources/contributors.txt | 1 + 2 files changed, 1 insertion(+) create mode 100644 resources/avatars/nealxm.png diff --git a/resources/avatars/nealxm.png b/resources/avatars/nealxm.png new file mode 100644 index 0000000000000000000000000000000000000000..fcba49189842ad18ae624e60c3cbc0a784b286e8 GIT binary patch literal 2689 zcmbW%c{J2}9|!Q?FwBf4VJx{ATZ|zwMxv3VE|a)stO;c+Yh6ONYf!|**w?PD!6->I zBcv=@Qk1P?lzj|k7n5}!-TRz-onT4*(z@&|d-m9S|2c zn1>evh4Jwp1h_!l++1L89v(1wzxl`h3;+xBh{$Uh@rvSIAPW9)baZMlRPl6mix{DA zSxMV9AO^-KE+KhH>aem3QWb^KIi`DDPv7{=*E(SR zFevzDNNCut+p%$X;uA<|=^2?>**Up+_bDZ%Wz_Nq4{M&%=(W%4>Kj^LwY7J2zV3R{ zKQQ>=C{*G;@YEJNI>Eb?y852K&e676{<}o3nra4f+p{@VFqlVPi&w-54{`AqRX|5W;iprJt6N}-+Jt2>*ML4gaV5;e;gz4HzeN8XDCYkX z{R8^P!(jjd+@Sr1aSH>6z$yb-Dx+pR+eV=uL6)6JFmZ6Sx z;Q+&g;B*ck!vVhaC!wr9{-zjF~&M;U4}q*buBnrccpKtbV;B@W=6JpX{mD&hby zeSF}waLGFxnUOOp8`e!@2zJlW4afaAwelfyLrdcCf#cXHB9*d@Cfe*I`G)rKGWwC% zPx}-6O={vf0L@>f+i1AOm2jKhCOpzT7mrUNaXu1K}M!W3bwm3j6 zVmOWMzDsO<2Av^AcF6BiFV)8c&Ji1%s(uhSz*Tc{#n09?Sz5-6M58M~+o`vD^zWS$ z5_`ZR!^gdDPZjmZ<)X#N9}SY>%{i98VWI{AayTu{ZWw4u;Q-+(Afo6_g+5*Ecs+W~ zDoDAgft=(~f7q{YOy+`r^p65mzCW9Tb>re+gJY`WIefJ#IBqr30)aFp>~=svTo)-LRi z&dlj9X5C?UE%Wuf&}vVxHx!?qPBsT~Uob&jxYpy$d)L;K=^B)#6Ql>W`a0O);^c4! zciKdMGrsgj93mu$4OzjgQ9cs|yF54n>@ zdlJ9IV(Y*Ht6`wQ5BJng++znmv%f0`zLPE!88ukne&QG*u`-aReI&LOCx2zw>k!|< z+wt|OCjXB#OwM@5dqo(u_Rrk>JH=CqKTLkuFnj4{E=uKoMu+V?+>g6b6`y=>_cMQV zj)Bbh#Y}HH+b8}{Eyn^gfdj;j#>AWS>z8h?FN;uos^Y1r&|Vyt#g>OzLlWH{RET>{ zz5&S}Zu6-tbhO1Dvuvq5>pxQ=ooTXX7Gs#{Wvu2mi93Z@>lZP^M>}7nYQ%rvEzzer zwad#JDXlEwLsvG<D))2u_qf-5R?FJLJccjiRj@a4|+tC*tJ(7qgc@2FUR;FWD?U_#?3*!B4 z+G+DJ@D}UE3B5xQMaJ{zaS3v%GbjZTcNb zb68ujTnJK?tp3Utc@q;FQ@)3;?;KaFUEZk`Dj#Na2KINMpT5?0`+el|0muNOCc6bU z%4|XvjI}JcAk!DD)Q+P1@=FkvN3ZHC4PnpfKxA3P8u{*xckOZONWlwavW_##&#=V6^M@&xM6E%~_mA0yAn8F6Yc6}5K)XoPU$Vx#M81X@MS`u1N zH4VM%PeT7JH_oh}Jl?jcGw%p#Ue{1l<^b(^I#CK{gQ?BisS(qAj*&s>yUEmp#NOil zzf0Ay2D4in*(na$*6FT!H$R^hUY4Rszbay0A6updMLyUNA}oOGmQQF;5Xuu`R71@x$MF}2CXpSFj3 zwe^WW?{Zel;qK7IiK;&|IG5tTWxM6Q6^^QIjD;OyEW(qU2TrFoV36vWd?hInjyE|TjV~-H*ImM9-!!TDJ9uoCCz~I zB6;~ZWDC``7y@7JgtuWy6rBcs3Z^#~A0?yxRQ5xl7G_cKtf^{eW*(t@ z&Ma4qFxncb5a`}*UZ)X;D4w(G!6s`PU8e^p7Z|@AyKLZ@CON%5Te-K~gF2)oB?R~(jllFRwCtuzgeN<;h zcm#r!D^@C?&S=IzG=RI(AKbdV;3wcY#R6~A-P9Kt(0kpN`(L+DE7luM3@ldwR>tWT zsCOD zxn*pTXr8Ud94__SDgJj}t@rAr9DC(T719$2o`FsQlZnX=mN(2@NI>ve<)u<|o&{3) S*a|5r`GvMJnHERn4F3yNQR+Pa literal 0 HcmV?d00001 diff --git a/resources/contributors.txt b/resources/contributors.txt index d60742040..9d131c7b9 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -76,6 +76,7 @@ crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png SputNikPlop | https://github.com/SputNikPlop | fraxx | https://github.com/fraxxio | :/avatars/fraxx.png KleberPF | https://github.com/KleberPF | +nealxm | https://github.com/nealxm | :/avatars/nealxm.png # If you are a contributor add yourself above this line From 4adc5be4d2843aab34c603377ff85efabb7c7c6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:31:48 +0200 Subject: [PATCH 38/71] chore(deps): bump DoozyX/clang-format-lint-action from 0.16.2 to 0.17 (#5320) Bumps [DoozyX/clang-format-lint-action](https://github.com/doozyx/clang-format-lint-action) from 0.16.2 to 0.17. - [Release notes](https://github.com/doozyx/clang-format-lint-action/releases) - [Commits](https://github.com/doozyx/clang-format-lint-action/compare/v0.16.2...v0.17) --- updated-dependencies: - dependency-name: DoozyX/clang-format-lint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-formatting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 9a3a36685..4ae1f1134 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -26,7 +26,7 @@ jobs: run: sudo apt-get -y install dos2unix - name: Check formatting - uses: DoozyX/clang-format-lint-action@v0.16.2 + uses: DoozyX/clang-format-lint-action@v0.17 with: source: "./src ./tests/src ./benchmarks/src ./mocks/include" extensions: "hpp,cpp" From e6bf503594da6bd02085d72d3924313cdd830e35 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:30:33 -0700 Subject: [PATCH 39/71] feat: include duration in more multi month gifts (#5319) * feat: include duration in more multi month gifts * chore: update sample data * chore: update changelog * push more sample data events to my channel * feat: use nicer display name for anon gifters --------- Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 50 +++++++++++++++++----- src/util/SampleData.cpp | 9 ++-- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 990cca03d..0edadcdb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Report sub duration for more multi-month gift cases. (#5319) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index afda22592..21a52ef1d 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -518,8 +518,7 @@ std::vector parseUserNoticeMessage(Channel *channel, { messageText = "Announcement"; } - else if (msgType == "subgift" && - ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); monthsIt != tags.end()) @@ -528,13 +527,29 @@ std::vector parseUserNoticeMessage(Channel *channel, if (months > 1) { auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); messageText = - QString("An anonymous user gifted %1 months of a Tier " - "%2 sub to %3!") - .arg(QString::number(months), + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), plan.isEmpty() ? '1' : plan.at(0), tags.value("msg-param-recipient-display-name") .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } } } } @@ -1032,8 +1047,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } - else if (msgType == "subgift" && - ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); monthsIt != tags.end()) @@ -1042,13 +1056,29 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, if (months > 1) { auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); messageText = - QString("An anonymous user gifted %1 months of a Tier " - "%2 sub to %3!") - .arg(QString::number(months), + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), plan.isEmpty() ? '1' : plan.at(0), tags.value("msg-param-recipient-display-name") .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } } } } diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 2c5b7ca4f..0b976f190 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -64,17 +64,20 @@ const QStringList &getSampleCheerMessages() const QStringList &getSampleSubMessages() { static QStringList list{ - R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=1337;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", + R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=11148817;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", R"(@badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=89614178;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\sof\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\sgifted\sa\sTier\s1\ssub\sto\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=13405587;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada)", // 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)", + 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 #pajlada)", // 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)", + 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 #pajlada)", + + // multi-month sub gift by broadcaster + R"(@user-id=35759863;msg-param-origin-id=2862055070165643340;display-name=Lucidfoxx;id=eeb3cdb8-337c-413a-9521-3a884ff78754;msg-param-gift-months=12;msg-param-sub-plan=1000;vip=0;emotes=;badges=broadcaster/1,subscriber/3042,partner/1;msg-param-recipient-user-name=ogprodigy;msg-param-recipient-id=53888434;badge-info=subscriber/71;room-id=35759863;msg-param-recipient-display-name=OGprodigy;msg-param-sub-plan-name=Silver\sPackage;subscriber=1;system-msg=Lucidfoxx\sgifted\sa\sTier\s1\ssub\sto\sOGprodigy!;login=lucidfoxx;msg-param-sender-count=0;user-type=;mod=0;flags=;rm-received-ts=1712803947891;color=#EB078D;msg-param-months=15;tmi-sent-ts=1712803947773;msg-id=subgift :tmi.twitch.tv USERNOTICE #pajlada)", // 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 1ca77a1e8417ce8c81ff36543ed7e99f42376b60 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 12 Apr 2024 23:05:47 +0200 Subject: [PATCH 40/71] Add context menu entry to toggle offline tabs (#5318) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/widgets/Notebook.cpp | 82 ++++++++++++++++++++++++++++++++++++++-- src/widgets/Notebook.hpp | 10 ++++- src/widgets/Window.cpp | 17 +-------- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0edadcdb7..cafe3e3e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index c16181f80..7fdff5495 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -33,7 +33,6 @@ namespace chatterino { Notebook::Notebook(QWidget *parent) : BaseWidget(parent) - , menu_(this) , addButton_(new NotebookButton(this)) { this->addButton_->setIcon(NotebookButton::Icon::Plus); @@ -81,8 +80,6 @@ Notebook::Notebook(QWidget *parent) << "Notebook must be created within a BaseWindow"; } - this->addNotebookActionsToMenu(&this->menu_); - // Manually resize the add button so the initial paint uses the correct // width when computing the maximum width occupied per column in vertical // tab rendering. @@ -1125,7 +1122,14 @@ void Notebook::mousePressEvent(QMouseEvent *event) switch (event->button()) { case Qt::RightButton: { - this->menu_.popup(event->globalPos() + QPoint(0, 8)); + event->accept(); + + if (!this->menu_) + { + this->menu_ = new QMenu(this); + this->addNotebookActionsToMenu(this->menu_); + } + this->menu_->popup(event->globalPos() + QPoint(0, 8)); } break; default:; @@ -1294,6 +1298,10 @@ SplitNotebook::SplitNotebook(Window *parent) this->addCustomButtons(); } + this->toggleOfflineTabsAction_ = new QAction({}, this); + QObject::connect(this->toggleOfflineTabsAction_, &QAction::triggered, this, + &SplitNotebook::toggleOfflineTabs); + getSettings()->tabVisibility.connect( [this](int val, auto) { auto visibility = NotebookTabVisibility(val); @@ -1307,12 +1315,17 @@ SplitNotebook::SplitNotebook(Window *parent) this->setTabVisibilityFilter([](const NotebookTab *tab) { return tab->isLive(); }); + this->toggleOfflineTabsAction_->setText("Show all tabs"); break; case NotebookTabVisibility::AllTabs: default: this->setTabVisibilityFilter(nullptr); + this->toggleOfflineTabsAction_->setText( + "Show live tabs only"); break; } + + this->updateToggleOfflineTabsHotkey(visibility); }, this->signalHolder_, true); @@ -1365,6 +1378,31 @@ SplitNotebook::SplitNotebook(Window *parent) }); } +void SplitNotebook::toggleOfflineTabs() +{ + if (!this->getShowTabs()) + { + // Tabs are currently hidden, so the intention is to show + // tabs again before enabling the live only setting + this->setShowTabs(true); + getSettings()->tabVisibility.setValue(NotebookTabVisibility::LiveOnly); + } + else + { + getSettings()->tabVisibility.setValue( + getSettings()->tabVisibility.getEnum() == + NotebookTabVisibility::LiveOnly + ? NotebookTabVisibility::AllTabs + : NotebookTabVisibility::LiveOnly); + } +} + +void SplitNotebook::addNotebookActionsToMenu(QMenu *menu) +{ + Notebook::addNotebookActionsToMenu(menu); + menu->addAction(this->toggleOfflineTabsAction_); +} + void SplitNotebook::showEvent(QShowEvent * /*event*/) { if (auto *page = this->getSelectedPage()) @@ -1442,6 +1480,42 @@ void SplitNotebook::addCustomButtons() this->updateStreamerModeIcon(); } +void SplitNotebook::updateToggleOfflineTabsHotkey( + NotebookTabVisibility newTabVisibility) +{ + auto *hotkeys = getIApp()->getHotkeys(); + auto getKeySequence = [&](auto argument) { + return hotkeys->getDisplaySequence(HotkeyCategory::Window, + "setTabVisibility", {{argument}}); + }; + + auto toggleSeq = getKeySequence("toggleLiveOnly"); + + switch (newTabVisibility) + { + case NotebookTabVisibility::AllTabs: + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("liveOnly"); + } + break; + + case NotebookTabVisibility::LiveOnly: + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("toggle"); + + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("on"); + } + } + break; + } + + this->toggleOfflineTabsAction_->setShortcut(toggleSeq); +} + void SplitNotebook::updateStreamerModeIcon() { if (this->streamerModeIcon_ == nullptr) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 9aa694c66..ac0162c42 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -118,7 +118,7 @@ public: bool isNotebookLayoutLocked() const; void setLockNotebookLayout(bool value); - void addNotebookActionsToMenu(QMenu *menu); + virtual void addNotebookActionsToMenu(QMenu *menu); // Update layout and tab visibility void refresh(); @@ -182,7 +182,7 @@ private: size_t visibleButtonCount() const; QList items_; - QMenu menu_; + QMenu *menu_ = nullptr; QWidget *selectedPage_ = nullptr; NotebookButton *addButton_; @@ -215,6 +215,9 @@ public: void select(QWidget *page, bool focusPage = true) override; void themeChangedEvent() override; + void addNotebookActionsToMenu(QMenu *menu) override; + void toggleOfflineTabs(); + protected: void showEvent(QShowEvent *event) override; @@ -223,6 +226,9 @@ private: pajlada::Signals::SignalHolder signalHolder_; + QAction *toggleOfflineTabsAction_; + void updateToggleOfflineTabsHotkey(NotebookTabVisibility newTabVisibility); + // Main window on Windows has basically a duplicate of this in Window NotebookButton *streamerModeIcon_{}; void updateStreamerModeIcon(); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 7106f872f..a9b0995dd 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -659,22 +659,7 @@ void Window::addShortcuts() } else if (arg == "toggleLiveOnly") { - if (!this->notebook_->getShowTabs()) - { - // Tabs are currently hidden, so the intention is to show - // tabs again before enabling the live only setting - this->notebook_->setShowTabs(true); - getSettings()->tabVisibility.setValue( - NotebookTabVisibility::LiveOnly); - } - else - { - getSettings()->tabVisibility.setValue( - getSettings()->tabVisibility.getEnum() == - NotebookTabVisibility::LiveOnly - ? NotebookTabVisibility::AllTabs - : NotebookTabVisibility::LiveOnly); - } + this->notebook_->toggleOfflineTabs(); } else { From f4e950ea0b06665c33c2b6ecc9b720daf3104276 Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 12 Apr 2024 23:48:08 +0200 Subject: [PATCH 41/71] Fix Wayland image upload crash if confirmation dialog is enabled (#5314) --- CHANGELOG.md | 1 + src/singletons/ImageUploader.cpp | 145 ++++++++++++++++++------------- src/singletons/ImageUploader.hpp | 10 ++- src/widgets/splits/Split.cpp | 20 ++++- 4 files changed, 111 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cafe3e3e1..3ed8fc68d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) +- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/singletons/ImageUploader.cpp b/src/singletons/ImageUploader.cpp index f8ad53d2e..3926df8f5 100644 --- a/src/singletons/ImageUploader.cpp +++ b/src/singletons/ImageUploader.cpp @@ -5,6 +5,7 @@ #include "common/network/NetworkRequest.hpp" #include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" +#include "debug/Benchmark.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Paths.hpp" @@ -21,6 +22,8 @@ #include #include +#include + #define UPLOAD_DELAY 2000 // Delay between uploads in milliseconds @@ -195,6 +198,11 @@ void ImageUploader::handleFailedUpload(const NetworkResult &result, } channel->addMessage(makeSystemMessage(errorMessage)); + // NOTE: We abort any future uploads on failure. Should this be handled differently? + while (!this->uploadQueue_.empty()) + { + this->uploadQueue_.pop(); + } this->uploadMutex_.unlock(); } @@ -248,22 +256,20 @@ void ImageUploader::handleSuccessfulUpload(const NetworkResult &result, this->logToFile(originalFilePath, link, deletionLink, channel); } -void ImageUploader::upload(const QMimeData *source, ChannelPtr channel, - QPointer outputTextEdit) +std::pair, QString> ImageUploader::getImages( + const QMimeData *source) const { - if (!this->uploadMutex_.tryLock()) - { - channel->addMessage(makeSystemMessage( - QString("Please wait until the upload finishes."))); - return; - } + BenchmarkGuard benchmarkGuard("ImageUploader::getImages"); - channel->addMessage(makeSystemMessage(QString("Started upload..."))); - auto tryUploadFromUrls = [&]() -> bool { + auto tryUploadFromUrls = + [&]() -> std::pair, QString> { if (!source->hasUrls()) { - return false; + return {{}, {}}; } + + std::queue images; + auto mimeDb = QMimeDatabase(); // This path gets chosen when files are copied from a file manager, like explorer.exe, caja. // Each entry in source->urls() is a QUrl pointing to a file that was copied. @@ -273,101 +279,118 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel, QMimeType mime = mimeDb.mimeTypeForUrl(path); if (mime.name().startsWith("image") && !mime.inherits("image/gif")) { - channel->addMessage(makeSystemMessage( - QString("Uploading image: %1").arg(localPath))); QImage img = QImage(localPath); if (img.isNull()) { - channel->addMessage( - makeSystemMessage(QString("Couldn't load image :("))); - return false; + return {{}, "Couldn't load image :("}; } auto imageData = convertToPng(img); - if (imageData) + if (!imageData) { - RawImageData data = {*imageData, "png", localPath}; - this->uploadQueue_.push(data); - } - else - { - channel->addMessage(makeSystemMessage( + return { + {}, QString("Cannot upload file: %1. Couldn't convert " "image to png.") - .arg(localPath))); - return false; + .arg(localPath), + }; } + images.push({*imageData, "png", localPath}); } else if (mime.inherits("image/gif")) { - channel->addMessage(makeSystemMessage( - QString("Uploading GIF: %1").arg(localPath))); QFile file(localPath); bool isOkay = file.open(QIODevice::ReadOnly); if (!isOkay) { - channel->addMessage( - makeSystemMessage(QString("Failed to open file. :("))); - return false; + return {{}, "Failed to open file :("}; } // file.readAll() => might be a bit big but it /should/ work - RawImageData data = {file.readAll(), "gif", localPath}; - this->uploadQueue_.push(data); + images.push({file.readAll(), "gif", localPath}); file.close(); } } - if (!this->uploadQueue_.empty()) - { - this->sendImageUploadRequest(this->uploadQueue_.front(), channel, - outputTextEdit); - this->uploadQueue_.pop(); - return true; - } - return false; + + return {images, {}}; }; - auto tryUploadDirectly = [&]() -> bool { + auto tryUploadDirectly = + [&]() -> std::pair, QString> { + std::queue images; + if (source->hasFormat("image/png")) { // the path to file is not present every time, thus the filePath is empty - this->sendImageUploadRequest({source->data("image/png"), "png", ""}, - channel, outputTextEdit); - return true; + images.push({source->data("image/png"), "png", ""}); + return {images, {}}; } + if (source->hasFormat("image/jpeg")) { - this->sendImageUploadRequest( - {source->data("image/jpeg"), "jpeg", ""}, channel, - outputTextEdit); - return true; + images.push({source->data("image/jpeg"), "jpeg", ""}); + return {images, {}}; } + if (source->hasFormat("image/gif")) { - this->sendImageUploadRequest({source->data("image/gif"), "gif", ""}, - channel, outputTextEdit); - return true; + images.push({source->data("image/gif"), "gif", ""}); + return {images, {}}; } + // not PNG, try loading it into QImage and save it to a PNG. auto image = qvariant_cast(source->imageData()); auto imageData = convertToPng(image); if (imageData) { - sendImageUploadRequest({*imageData, "png", ""}, channel, - outputTextEdit); - return true; + images.push({*imageData, "png", ""}); + return {images, {}}; } + // No direct upload happenned - channel->addMessage(makeSystemMessage( - QString("Cannot upload file, failed to convert to png."))); - return false; + return {{}, "Cannot upload file, failed to convert to png."}; }; - if (!tryUploadFromUrls() && !tryUploadDirectly()) + const auto [urlImageData, urlError] = tryUploadFromUrls(); + + if (!urlImageData.empty()) { - channel->addMessage( - makeSystemMessage(QString("Cannot upload file from clipboard."))); - this->uploadMutex_.unlock(); + return {urlImageData, {}}; } + + const auto [directImageData, directError] = tryUploadDirectly(); + if (!directImageData.empty()) + { + return {directImageData, {}}; + } + + return { + {}, + // TODO: verify that this looks ok xd + urlError + directError, + }; +} + +void ImageUploader::upload(std::queue images, ChannelPtr channel, + QPointer outputTextEdit) +{ + BenchmarkGuard benchmarkGuard("upload"); + if (!this->uploadMutex_.tryLock()) + { + channel->addMessage(makeSystemMessage( + QString("Please wait until the upload finishes."))); + return; + } + + assert(!images.empty()); + assert(this->uploadQueue_.empty()); + + std::swap(this->uploadQueue_, images); + + channel->addMessage(makeSystemMessage("Started upload...")); + + this->sendImageUploadRequest(this->uploadQueue_.front(), std::move(channel), + std::move(outputTextEdit)); + this->uploadQueue_.pop(); } } // namespace chatterino diff --git a/src/singletons/ImageUploader.hpp b/src/singletons/ImageUploader.hpp index 260180583..41f4c8b60 100644 --- a/src/singletons/ImageUploader.hpp +++ b/src/singletons/ImageUploader.hpp @@ -25,8 +25,16 @@ struct RawImageData { class ImageUploader final : public Singleton { public: + /** + * Tries to get the image(s) from the given QMimeData + * + * If no images were found, the second value in the pair will contain an error message + */ + std::pair, QString> getImages( + const QMimeData *source) const; + void save() override; - void upload(const QMimeData *source, ChannelPtr channel, + void upload(std::queue images, ChannelPtr channel, QPointer outputTextEdit); private: diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 9e3126319..1e4c228ef 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -380,12 +380,26 @@ Split::Split(QWidget *parent) // this connection can be ignored since the SplitInput is owned by this Split std::ignore = this->input_->ui_.textEdit->imagePasted.connect( - [this](const QMimeData *source) { + [this](const QMimeData *original) { if (!getSettings()->imageUploaderEnabled) { return; } + auto channel = this->getChannel(); + auto *imageUploader = getIApp()->getImageUploader(); + + auto [images, imageProcessError] = + imageUploader->getImages(original); + if (images.empty()) + { + channel->addMessage(makeSystemMessage( + QString( + "An error occurred trying to process your image: %1") + .arg(imageProcessError))); + return; + } + if (getSettings()->askOnImageUpload.getValue()) { QMessageBox msgBox(this->window()); @@ -427,9 +441,9 @@ Split::Split(QWidget *parent) return; } } + QPointer edit = this->input_->ui_.textEdit; - getIApp()->getImageUploader()->upload(source, this->getChannel(), - edit); + imageUploader->upload(std::move(images), channel, edit); }); getSettings()->imageUploaderEnabled.connect( From bf8266e9b3fbf3e68166bdc90038cf848a9f4bf9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 13 Apr 2024 09:01:20 +0200 Subject: [PATCH 42/71] fix: improve error messaging when pgrep fails streamer mode detection (#5321) When launching pgrep times out, a timeout message is now posted instead of the default "pgrep is not installed" error. --- CHANGELOG.md | 1 + src/singletons/StreamerMode.cpp | 49 ++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed8fc68d..803cdb60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) +- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) - Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index 7ee4fa588..4eda9e3bd 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -51,6 +51,9 @@ const QStringList &broadcastingBinaries() bool isBroadcasterSoftwareActive() { #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + static bool shouldShowTimeoutWarning = true; + static bool shouldShowWarning = true; + QProcess p; p.start("pgrep", {"-xi", broadcastingBinaries().join("|")}, QIODevice::NotOpen); @@ -62,20 +65,46 @@ bool isBroadcasterSoftwareActive() // Fallback to false and showing a warning - static bool shouldShowWarning = true; - if (shouldShowWarning) + switch (p.error()) { - shouldShowWarning = false; + case QProcess::Timedout: { + qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; + if (shouldShowTimeoutWarning) + { + shouldShowTimeoutWarning = false; - postToThread([] { - getApp()->twitch->addGlobalSystemMessage( - "Streamer Mode is set to Automatic, but pgrep is missing. " - "Install it to fix the issue or set Streamer Mode to " - "Enabled or Disabled in the Settings."); - }); + postToThread([] { + getApp()->twitch->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep timed " + "out. This can happen if your system lagged at the " + "wrong moment. If Streamer Mode continues to not work, " + "you can manually set it to Enabled or Disabled in the " + "Settings."); + }); + } + } + break; + + default: { + qCWarning(chatterinoStreamerMode) + << "pgrep execution failed:" << p.error(); + + if (shouldShowWarning) + { + shouldShowWarning = false; + + postToThread([] { + getApp()->twitch->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep is " + "missing. " + "Install it to fix the issue or set Streamer Mode to " + "Enabled or Disabled in the Settings."); + }); + } + } + break; } - qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; return false; #elif defined(Q_OS_WIN) if (!IsWindowsVistaOrGreater()) From 7285f08a043e726ce04857cb27b86b69d80b0280 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 13 Apr 2024 10:18:34 +0200 Subject: [PATCH 43/71] Fixed links having `http://` added to the beginning in certain cases. (#5323) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803cdb60e..3dfc30ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) +- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) ## 2.5.0-beta.1 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index cc71558f6..45b8cafda 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -635,7 +635,7 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) auto textColor = MessageColor(MessageColor::Link); auto *el = this->emplace( LinkElement::Parsed{.lowercase = lowercaseLinkString, - .original = matchedLink}, + .original = origLink}, MessageElementFlag::Text, textColor); el->setLink({Link::Url, matchedLink}); getIApp()->getLinkResolver()->resolve(el->linkInfo()); From c391ff9740bdc2f01ed6c82516340f1a97195cca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 23:05:11 -0400 Subject: [PATCH 44/71] chore(deps): bump lib/settings from `ceac9c7` to `70fbc72` (#5325) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index ceac9c7e9..70fbc7236 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit ceac9c7e97d2d2b97f40ecd0b421e358d7525cbc +Subproject commit 70fbc7236aa8bcf5db4748e7f56dad132d6fd402 From b391f18177a0b3870e6a1cd4d48f7c1a622ec87d Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 17 Apr 2024 19:08:47 +0200 Subject: [PATCH 45/71] fix: set maximum of scrollbar after filtering (#5329) --- CHANGELOG.md | 1 + src/messages/LimitedQueue.hpp | 16 ++++++++-------- src/widgets/helper/ChannelView.cpp | 7 +++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfc30ca4..2cf916103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) ## 2.5.0-beta.1 diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 62fd02527..e06e5a0f2 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -24,14 +24,6 @@ public: private: /// Property Accessors - /** - * @brief Return the limit of the internal buffer - */ - [[nodiscard]] size_t limit() const - { - return this->limit_; - } - /** * @brief Return the amount of space left in the buffer * @@ -43,6 +35,14 @@ private: } public: + /** + * @brief Return the limit of the queue + */ + [[nodiscard]] size_t limit() const + { + return this->limit_; + } + /** * @brief Return true if the buffer is empty */ diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c76373d56..c5a679727 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -982,8 +982,7 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) // and the ui. auto snapshot = underlyingChannel->getMessageSnapshot(); - this->scrollBar_->setMaximum(qreal(snapshot.size())); - + size_t nMessagesAdded = 0; for (const auto &msg : snapshot) { if (!this->shouldIncludeMessage(msg)) @@ -1007,12 +1006,16 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->messages_.pushBack(messageLayout); this->channel_->addMessage(msg); + nMessagesAdded++; if (this->showScrollbarHighlights()) { this->scrollBar_->addHighlight(msg->getScrollBarHighlight()); } } + this->scrollBar_->setMaximum( + static_cast(std::min(nMessagesAdded, this->messages_.limit()))); + // // Standard channel connections // From 86a27823a27fc7bc174edfe92b9d3b3601addfc5 Mon Sep 17 00:00:00 2001 From: nerix Date: Thu, 18 Apr 2024 16:24:12 +0200 Subject: [PATCH 46/71] fix: don't change the topmost value of child windows (#5330) --- CHANGELOG.md | 1 + src/widgets/BaseWindow.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf916103..b3d60bdc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) ## 2.5.0-beta.1 diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index a61322fce..81ed90b28 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -263,6 +263,13 @@ void BaseWindow::tryApplyTopMost() } this->waitingForTopMost_ = false; + if (this->parent()) + { + // Don't change the topmost value of child windows. This would apply + // to the top-level window too. + return; + } + ::SetWindowPos(*hwnd, this->isTopMost_ ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } From 7c97e6bcc748755ee55447096ebc657be11b4789 Mon Sep 17 00:00:00 2001 From: Maverick Date: Thu, 18 Apr 2024 17:49:50 +0200 Subject: [PATCH 47/71] Change order of query parameters of Twitch Player URLs. (#5326) This ensures that it doesn't "fake redirect". Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/common/Common.hpp | 3 +++ src/providers/twitch/TwitchChannel.cpp | 3 +-- src/singletons/Toasts.cpp | 6 +++--- src/widgets/splits/Split.cpp | 3 +-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d60bdc7..8c19a0de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) - Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) ## 2.5.0-beta.1 diff --git a/src/common/Common.hpp b/src/common/Common.hpp index 35b8efb1c..8d6097473 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -14,6 +14,9 @@ namespace chatterino { +const inline auto TWITCH_PLAYER_URL = + QStringLiteral("https://player.twitch.tv/?channel=%1&parent=twitch.tv"); + enum class HighlightState { None, Highlighted, diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 39350b69c..c478b9191 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -84,8 +84,7 @@ TwitchChannel::TwitchChannel(const QString &name) , nameOptions{name, name, name} , subscriptionUrl_("https://www.twitch.tv/subs/" + name) , channelUrl_("https://twitch.tv/" + name) - , popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" + - name) + , popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name)) , bttvEmotes_(std::make_shared()) , ffzEmotes_(std::make_shared()) , seventvEmotes_(std::make_shared()) diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index 51dbf4680..3ca5b6e0c 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -1,6 +1,7 @@ #include "Toasts.hpp" #include "Application.hpp" +#include "common/Common.hpp" #include "common/Literals.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" @@ -177,9 +178,8 @@ public: case ToastReaction::OpenInPlayer: if (platform_ == Platform::Twitch) { - QDesktopServices::openUrl(QUrl( - u"https://player.twitch.tv/?parent=twitch.tv&channel=" % - channelName_)); + QDesktopServices::openUrl( + QUrl(TWITCH_PLAYER_URL.arg(channelName_))); } break; case ToastReaction::OpenInStreamlink: { diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 1e4c228ef..81b0e2f65 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -836,8 +836,7 @@ void Split::openChannelInBrowserPlayer(ChannelPtr channel) if (auto *twitchChannel = dynamic_cast(channel.get())) { QDesktopServices::openUrl( - "https://player.twitch.tv/?parent=twitch.tv&channel=" + - twitchChannel->getName()); + QUrl(TWITCH_PLAYER_URL.arg(twitchChannel->getName()))); } } From 992ea88884052058c709986e199fa23ebac2b8a9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 20 Apr 2024 11:14:23 +0200 Subject: [PATCH 48/71] fix: Remove "Show chatter list" entry from split header menu for non-mods (#5336) --- CHANGELOG.md | 1 + src/widgets/splits/SplitHeader.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c19a0de8..f76498cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) - Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) - Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) ## 2.5.0-beta.1 diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index ed4a59b18..6b7021798 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -525,9 +525,12 @@ std::unique_ptr SplitHeader::createMainMenu() if (twitchChannel) { - moreMenu->addAction( - "Show chatter list", this->split_, &Split::showChatterList, - h->getDisplaySequence(HotkeyCategory::Split, "openViewerList")); + if (twitchChannel->hasModRights()) + { + moreMenu->addAction( + "Show chatter list", this->split_, &Split::showChatterList, + h->getDisplaySequence(HotkeyCategory::Split, "openViewerList")); + } moreMenu->addAction("Subscribe", this->split_, &Split::openSubPage); From 48cbb7f8d1053d713e4a734675be8f30e59fdf06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 09:40:35 +0000 Subject: [PATCH 49/71] chore(deps): bump lib/lua/src from `e288c5a` to `0897c0a` (#5316) --- lib/lua/src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lua/src b/lib/lua/src index e288c5a91..0897c0a42 160000 --- a/lib/lua/src +++ b/lib/lua/src @@ -1 +1 @@ -Subproject commit e288c5a91883793d14ed9e9d93464f6ee0b08915 +Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60 From 3aead09339ffff0fbfd9f8a7a5ea9eb551ad8fb8 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 11:17:25 +0200 Subject: [PATCH 50/71] Release v2.5.0 (#5337) --- CHANGELOG.md | 25 +++++++++---------- .../com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76498cc5..3fed1f751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,7 @@ ## Unversioned -- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) -- Minor: Report sub duration for more multi-month gift cases. (#5319) -- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) -- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) -- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) -- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) -- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) -- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) -- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) -- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) -- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) - -## 2.5.0-beta.1 +## 2.5.0 - 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) @@ -68,6 +56,16 @@ - 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) +- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) +- Minor: Report sub duration for more multi-month gift cases. (#5319) +- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) +- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) +- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) +- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) +- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) +- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) - 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) @@ -138,6 +136,7 @@ - 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: Changed the order of the query parameters for Twitch player URLs. (#5326) - 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/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 5f76c9f6d..69d1b9289 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0 + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 3673e5b23..b58af4fbd 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.5.0-beta.1" +#define CHATTERINO_VERSION "2.5.0" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From 1a04bda56ba82422f37ce09d68453efb3b6932cd Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 18:30:31 +0200 Subject: [PATCH 51/71] fix: use openssl3 on qt6 windows builds (#5340) regression since 2.4.6 --- .github/workflows/build.yml | 9 ++------- .github/workflows/test-windows.yml | 8 ++------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1581eddf..0a7408d22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,6 +139,8 @@ jobs: C2_PLUGINS: ${{ matrix.plugins }} C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }} C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} steps: - uses: actions/checkout@v4 @@ -188,13 +190,6 @@ jobs: if: startsWith(matrix.os, 'windows') uses: ilammy/msvc-dev-cmd@v1.13.0 - - name: Setup conan variables (Windows) - if: startsWith(matrix.os, 'windows') - run: | - "C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV" - "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" - shell: powershell - - name: Setup sccache (Windows) # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 12ddb164c..f1c76e8cb 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,6 +32,8 @@ jobs: env: C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }} QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} steps: - name: Enable plugin support @@ -65,12 +67,6 @@ jobs: - name: Enable Developer Command Prompt uses: ilammy/msvc-dev-cmd@v1.13.0 - - name: Setup conan variables - if: startsWith(matrix.os, 'windows') - run: | - "C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV" - "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" - - name: Setup sccache # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 From dfa929e20736e3da93a97e12de288974dcc69871 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 21 Apr 2024 21:24:11 +0200 Subject: [PATCH 52/71] fix: use the full url when resolving (#5345) --- CHANGELOG.md | 2 ++ src/common/LinkParser.hpp | 19 +++++++++++++++++++ src/messages/MessageBuilder.cpp | 9 ++++----- src/messages/MessageElement.cpp | 7 ++++--- src/messages/MessageElement.hpp | 5 ++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fed1f751..d335d8274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Bugfix: Fixed links without a protocol not being clickable. (#5345) + ## 2.5.0 - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 16bfe235e..9d5e10cfb 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -12,9 +12,28 @@ struct ParsedLink { #else using StringView = QStringRef; #endif + /// The parsed protocol of the link. Can be empty. + /// + /// https://www.forsen.tv/commands + /// ^------^ StringView protocol; + + /// The parsed host of the link. Can not be empty. + /// + /// https://www.forsen.tv/commands + /// ^-----------^ StringView host; + + /// The remainder of the link. Can be empty. + /// + /// https://www.forsen.tv/commands + /// ^-------^ StringView rest; + + /// The original unparsed link. + /// + /// https://www.forsen.tv/commands + /// ^----------------------------^ QString source; }; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 45b8cafda..e17194549 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -617,16 +617,16 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) { QString lowercaseLinkString; QString origLink = parsedLink.source; - QString matchedLink; + QString fullUrl; if (parsedLink.protocol.isNull()) { - matchedLink = QStringLiteral("http://") + parsedLink.source; + fullUrl = QStringLiteral("http://") + parsedLink.source; } else { lowercaseLinkString += parsedLink.protocol; - matchedLink = parsedLink.source; + fullUrl = parsedLink.source; } lowercaseLinkString += parsedLink.host.toString().toLower(); @@ -636,8 +636,7 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) auto *el = this->emplace( LinkElement::Parsed{.lowercase = lowercaseLinkString, .original = origLink}, - MessageElementFlag::Text, textColor); - el->setLink({Link::Url, matchedLink}); + fullUrl, MessageElementFlag::Text, textColor); getIApp()->getLinkResolver()->resolve(el->linkInfo()); } diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 56d1e2ed3..b11e82be1 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -679,10 +679,11 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container, } } -LinkElement::LinkElement(const Parsed &parsed, MessageElementFlags flags, - const MessageColor &color, FontStyle style) +LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, const MessageColor &color, + FontStyle style) : TextElement({}, flags, color, style) - , linkInfo_(parsed.original) + , linkInfo_(fullUrl) , lowercase_({parsed.lowercase}) , original_({parsed.original}) { diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 68f90e9b5..b57bab752 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -272,7 +272,10 @@ public: QString original; }; - LinkElement(const Parsed &parsed, MessageElementFlags flags, + /// @param parsed The link as it appeared in the message + /// @param fullUrl A full URL (notably with a protocol) + LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, const MessageColor &color = MessageColor::Text, FontStyle style = FontStyle::ChatMedium); ~LinkElement() override = default; From 4a1ce2a3b322c6e16a6b96245608829fe9bb8d17 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 22:52:44 +0200 Subject: [PATCH 53/71] refactor: clean up toStdString usages in tests (#5346) * tests: Add QString/QStringView << operators for std::ostream This makes it easier to print QString/QStringView's in ASSERT_EQ outputs * tests: clean up toStdString usages * fix: use QByteArray.toStdString instead --- tests/src/Emojis.cpp | 6 +++--- tests/src/LinkParser.cpp | 14 ++++++++------ tests/src/TestHelpers.hpp | 22 ++++++++++++++++++++++ tests/src/TwitchMessageBuilder.cpp | 20 ++++++++++---------- tests/src/XDGHelper.cpp | 7 ++++--- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/tests/src/Emojis.cpp b/tests/src/Emojis.cpp index 0f6cf6762..141a64afb 100644 --- a/tests/src/Emojis.cpp +++ b/tests/src/Emojis.cpp @@ -1,6 +1,7 @@ #include "providers/emoji/Emojis.hpp" #include "common/Literals.hpp" +#include "TestHelpers.hpp" #include #include @@ -53,7 +54,7 @@ TEST(Emojis, ShortcodeParsing) } EXPECT_EQ(output, test.expectedOutput) - << "Input " << test.input.toStdString() << " failed"; + << "Input " << test.input << " failed"; } } @@ -165,8 +166,7 @@ TEST(Emojis, Parse) // can't use EXPECT_EQ because EmotePtr can't be printed if (output != test.expectedOutput) { - EXPECT_TRUE(false) - << "Input " << test.input.toStdString() << " failed"; + EXPECT_TRUE(false) << "Input " << test.input << " failed"; } } } diff --git a/tests/src/LinkParser.cpp b/tests/src/LinkParser.cpp index 0931ef859..cce5c8c6c 100644 --- a/tests/src/LinkParser.cpp +++ b/tests/src/LinkParser.cpp @@ -1,5 +1,7 @@ #include "common/LinkParser.hpp" +#include "TestHelpers.hpp" + #include #include #include @@ -15,13 +17,13 @@ struct Case { { auto input = this->protocol + this->host + this->rest; LinkParser p(input); - ASSERT_TRUE(p.result().has_value()) << input.toStdString(); + ASSERT_TRUE(p.result().has_value()) << input; const auto &r = *p.result(); ASSERT_EQ(r.source, input); - ASSERT_EQ(r.protocol, this->protocol) << this->protocol.toStdString(); - ASSERT_EQ(r.host, this->host) << this->host.toStdString(); - ASSERT_EQ(r.rest, this->rest) << this->rest.toStdString(); + ASSERT_EQ(r.protocol, this->protocol) << this->protocol; + ASSERT_EQ(r.host, this->host) << this->host; + ASSERT_EQ(r.rest, this->rest) << this->rest; } }; @@ -126,7 +128,7 @@ TEST(LinkParser, doesntParseInvalidIpv4Links) for (const auto &input : inputs) { LinkParser p(input); - ASSERT_FALSE(p.result().has_value()) << input.toStdString(); + ASSERT_FALSE(p.result().has_value()) << input; } } @@ -170,6 +172,6 @@ TEST(LinkParser, doesntParseInvalidLinks) for (const auto &input : inputs) { LinkParser p(input); - ASSERT_FALSE(p.result().has_value()) << input.toStdString(); + ASSERT_FALSE(p.result().has_value()) << input; } } diff --git a/tests/src/TestHelpers.hpp b/tests/src/TestHelpers.hpp index 05190e0da..30a2d0b1a 100644 --- a/tests/src/TestHelpers.hpp +++ b/tests/src/TestHelpers.hpp @@ -1,6 +1,10 @@ #pragma once +#include +#include + #include +#include template class ReceivedMessage @@ -42,3 +46,21 @@ public: return &this->t; } }; + +inline std::ostream &operator<<(std::ostream &os, const QStringView &str) +{ + os << qUtf8Printable(str.toString()); + return os; +} + +inline std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) +{ + os << bytes.toStdString(); + return os; +} + +inline std::ostream &operator<<(std::ostream &os, const QString &str) +{ + os << qUtf8Printable(str); + return os; +} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index 7b6b42c33..77ddcdf46 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -15,6 +15,7 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Emotes.hpp" +#include "TestHelpers.hpp" #include #include @@ -147,7 +148,7 @@ TEST(TwitchMessageBuilder, CommaSeparatedListTagParsing) auto output = TwitchMessageBuilder::slashKeyValue(test.input); EXPECT_EQ(output, test.expectedOutput) - << "Input " << test.input.toStdString() << " failed"; + << "Input " << test.input << " failed"; } } @@ -230,12 +231,12 @@ TEST(TwitchMessageBuilder, BadgeInfoParsing) auto outputBadgeInfo = TwitchMessageBuilder::parseBadgeInfoTag(privmsg->tags()); EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo) - << "Input for badgeInfo " << test.input.toStdString() << " failed"; + << "Input for badgeInfo " << test.input << " failed"; auto outputBadges = SharedMessageBuilder::parseBadgeTag(privmsg->tags()); EXPECT_EQ(outputBadges, test.expectedBadges) - << "Input for badges " << test.input.toStdString() << " failed"; + << "Input for badges " << test.input << " failed"; delete privmsg; } @@ -413,8 +414,7 @@ TEST_F(TestTwitchMessageBuilder, ParseTwitchEmotes) privmsg->tags(), originalMessage, 0); EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes) - << "Input for twitch emotes " << test.input.toStdString() - << " failed"; + << "Input for twitch emotes " << test.input << " failed"; delete privmsg; } @@ -617,11 +617,11 @@ TEST_F(TestTwitchMessageBuilder, IgnoresReplace) emotes); EXPECT_EQ(message, test.expectedMessage) - << "Message not equal for input '" << test.input.toStdString() - << "' - expected: '" << test.expectedMessage.toStdString() - << "' got: '" << message.toStdString() << "'"; + << "Message not equal for input '" << test.input + << "' - expected: '" << test.expectedMessage << "' got: '" + << message << "'"; EXPECT_EQ(emotes, test.expectedTwitchEmotes) - << "Twitch emotes not equal for input '" << test.input.toStdString() - << "' and output '" << message.toStdString() << "'"; + << "Twitch emotes not equal for input '" << test.input + << "' and output '" << message << "'"; } } diff --git a/tests/src/XDGHelper.cpp b/tests/src/XDGHelper.cpp index a8bcac801..3926d21d9 100644 --- a/tests/src/XDGHelper.cpp +++ b/tests/src/XDGHelper.cpp @@ -1,5 +1,7 @@ #include "util/XDGHelper.hpp" +#include "TestHelpers.hpp" + #include #include @@ -57,9 +59,8 @@ TEST(XDGHelper, ParseDesktopExecProgram) auto output = parseDesktopExecProgram(test.input); EXPECT_EQ(output, test.expectedOutput) - << "Input '" << test.input.toStdString() << "' failed. Expected '" - << test.expectedOutput.toStdString() << "' but got '" - << output.toStdString() << "'"; + << "Input '" << test.input << "' failed. Expected '" + << test.expectedOutput << "' but got '" << output << "'"; } } From 1caf7ca4d6f10d520c5711389b930a6c25eaca66 Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Mon, 22 Apr 2024 01:07:35 -0400 Subject: [PATCH 54/71] fix: warning in homebrew workflow (#5347) --- .github/workflows/homebrew.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index b455baaec..6da0e71d3 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -26,4 +26,5 @@ jobs: echo "Running bump-cask-pr for cask '$C2_CASK_NAME' and version '$C2_TAGGED_VERSION'" C2_TAGGED_VERSION_STRIPPED="${C2_TAGGED_VERSION:1}" echo "Stripped version: '$C2_TAGGED_VERSION_STRIPPED'" + brew developer on brew bump-cask-pr --version "$C2_TAGGED_VERSION_STRIPPED" "$C2_CASK_NAME" From 58a930d28cdfe0daa32c5ecdd1aa2045f55f3f26 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 26 Apr 2024 15:15:09 +0200 Subject: [PATCH 55/71] ci: pin to macos-13 (#5362) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a7408d22..53d9558b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,7 +112,7 @@ jobs: matrix: include: # macOS - - os: macos-latest + - os: macos-13 qt-version: 5.15.2 force-lto: false plugins: false From eafcb941f57011358d63c76de6bee38ca1ba97ec Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 28 Apr 2024 11:36:58 +0200 Subject: [PATCH 56/71] Release v2.5.1 (#5364) --- .CI/chatterino-installer.iss | 2 +- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 +- resources/com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss index fddd668f9..1f9816a29 100644 --- a/.CI/chatterino-installer.iss +++ b/.CI/chatterino-installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Chatterino" -#define MyAppVersion "2.5.0" +#define MyAppVersion "2.5.1" #define MyAppPublisher "Chatterino Team" #define MyAppURL "https://www.chatterino.com" #define MyAppExeName "chatterino.exe" diff --git a/CHANGELOG.md b/CHANGELOG.md index d335d8274..07df030e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.5.1 + - Bugfix: Fixed links without a protocol not being clickable. (#5345) ## 2.5.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a08cf79e..fdcb7075b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ if(BUILD_BENCHMARKS) endif() project(chatterino - VERSION 2.5.0 + VERSION 2.5.1 DESCRIPTION "Chat client for twitch.tv" HOMEPAGE_URL "https://chatterino.com/" ) diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 69d1b9289..a2d09fecb 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.1 + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index b58af4fbd..fbe536a69 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.5.0" +#define CHATTERINO_VERSION "2.5.1" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From 80cf1e533c5efd5ffe661e92fb8b7dc4748738d1 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 28 Apr 2024 13:36:01 +0200 Subject: [PATCH 57/71] ci: add workflow for WinGet (#5365) --- .github/workflows/winget.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/winget.yml diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 000000000..7e8a5091a --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,14 @@ +name: Publish to WinGet +on: + release: + types: [released] +jobs: + publish: + runs-on: windows-latest + if: ${{ startsWith(github.event.release.tag_name, 'v') }} + steps: + - uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: ChatterinoTeam.Chatterino + installers-regex: ^Chatterino.Installer.exe$ + token: ${{ secrets.WINGET_TOKEN }} From b5066881f947d3f3cdc13eda3a16ed7d84ce5471 Mon Sep 17 00:00:00 2001 From: fossdd Date: Tue, 30 Apr 2024 21:14:50 +0000 Subject: [PATCH 58/71] docs: add `https://`to github URL (#5368) --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index f9bc5798a..47ac1b202 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,4 +7,4 @@ docker run --network=host --detach ghcr.io/chatterino/twitch-pubsub-server-test: docker run -p 9051:80 --detach kennethreitz/httpbin ``` -If you're unable to use docker, you can use [httpbox](github.com/kevinastone/httpbox) (`httpbox --port 9051`) and [Chatterino/twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) manually. +If you're unable to use docker, you can use [httpbox](https://github.com/kevinastone/httpbox) (`httpbox --port 9051`) and [Chatterino/twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) manually. From 85cb2a1f3cea22b6aea66f211140da05f448cef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 10:11:56 +0000 Subject: [PATCH 59/71] chore(deps): bump hendrikmuhs/ccache-action from 1.2.12 to 1.2.13 (#5370) Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.12 to 1.2.13. - [Release notes](https://github.com/hendrikmuhs/ccache-action/releases) - [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.12...v1.2.13) --- updated-dependencies: - dependency-name: hendrikmuhs/ccache-action 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> Co-authored-by: pajlada --- .github/workflows/build.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53d9558b7..3e42b5dd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,7 +192,7 @@ jobs: - name: Setup sccache (Windows) # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.12 + uses: hendrikmuhs/ccache-action@v1.2.13 if: startsWith(matrix.os, 'windows') with: variant: sccache diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f1c76e8cb..0b73ee84f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,7 +69,7 @@ jobs: - name: Setup sccache # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.12 + uses: hendrikmuhs/ccache-action@v1.2.13 with: variant: sccache # only save on the default (master) branch From a88a2ac65ca283a2c68db7479800f870a610f421 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 4 May 2024 12:39:14 +0200 Subject: [PATCH 60/71] build: add doxygen target (#5377) Co-authored-by: Nerixyz --- CHANGELOG.md | 2 ++ CMakeLists.txt | 1 + src/CMakeLists.txt | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07df030e0..0a5d65fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Dev: Add doxygen build target. (#5377) + ## 2.5.1 - Bugfix: Fixed links without a protocol not being clickable. (#5345) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdcb7075b..eec6b7e1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ find_package(PajladaSerialize REQUIRED) find_package(PajladaSignals REQUIRED) find_package(LRUCache REQUIRED) find_package(MagicEnum REQUIRED) +find_package(Doxygen) if (USE_SYSTEM_PAJLADA_SETTINGS) find_package(PajladaSettings REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb64bcf31..301808906 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1141,3 +1141,14 @@ if(NOT CHATTERINO_UPDATER) message(STATUS "Disabling the updater.") target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_DISABLE_UPDATER) endif() + +if (DOXYGEN_FOUND) + message(STATUS "Doxygen found, adding doxygen target") + # output will be in docs/html + set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs") + + doxygen_add_docs( + doxygen + ${CMAKE_CURRENT_LIST_DIR} + ) +endif () From 401feac0aadffc381d2921a2cae3dfa12104aa1d Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 5 May 2024 15:01:07 +0200 Subject: [PATCH 61/71] tests: better test initializing allowing for better printing (#5379) Co-authored-by: Nerixyz --- CHANGELOG.md | 1 + tests/CMakeLists.txt | 3 +- tests/src/AccessGuard.cpp | 2 +- tests/src/BasicPubSub.cpp | 2 +- tests/src/BttvLiveUpdates.cpp | 3 +- tests/src/ChannelChatters.cpp | 2 +- tests/src/ChatterSet.cpp | 3 +- tests/src/Emojis.cpp | 3 +- tests/src/ExponentialBackoff.cpp | 2 +- tests/src/Filters.cpp | 43 +++++++++---------- tests/src/FormatTime.cpp | 13 +++--- tests/src/Helpers.cpp | 15 ++++--- tests/src/HighlightController.cpp | 9 ++-- tests/src/HighlightPhrase.cpp | 2 +- tests/src/Hotkeys.cpp | 3 +- tests/src/InputCompletion.cpp | 4 +- tests/src/IrcHelpers.cpp | 7 ++-- tests/src/LimitedQueue.cpp | 2 +- tests/src/LinkInfo.cpp | 3 +- tests/src/LinkParser.cpp | 3 +- tests/src/Literals.cpp | 2 +- tests/src/MessageLayout.cpp | 2 +- tests/src/NetworkCommon.cpp | 2 +- tests/src/NetworkRequest.cpp | 2 +- tests/src/NetworkResult.cpp | 2 +- tests/src/NotebookTab.cpp | 3 +- tests/src/QMagicEnum.cpp | 3 +- tests/src/RatelimitBucket.cpp | 3 +- tests/src/Selection.cpp | 2 +- tests/src/SeventvEventAPI.cpp | 2 +- tests/src/SplitInput.cpp | 8 ++-- tests/src/Test.cpp | 42 +++++++++++++++++++ tests/src/Test.hpp | 22 ++++++++++ tests/src/TestHelpers.hpp | 66 ------------------------------ tests/src/TwitchMessageBuilder.cpp | 3 +- tests/src/TwitchPubSubClient.cpp | 45 +++++++++++++++++++- tests/src/Updates.cpp | 2 +- tests/src/UtilTwitch.cpp | 32 ++++++--------- tests/src/XDGDesktopFile.cpp | 3 +- tests/src/XDGHelper.cpp | 3 +- tests/src/main.cpp | 2 +- 41 files changed, 198 insertions(+), 178 deletions(-) create mode 100644 tests/src/Test.cpp create mode 100644 tests/src/Test.hpp delete mode 100644 tests/src/TestHelpers.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5d65fa2..56d2b0054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Dev: Add doxygen build target. (#5377) +- Dev: Make printing of strings in tests easier. (#5379) ## 2.5.1 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fb5730048..bd35b79de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,8 @@ option(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN "Use public httpbin for testing networ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp ${CMAKE_CURRENT_LIST_DIR}/resources/test-resources.qrc - ${CMAKE_CURRENT_LIST_DIR}/src/TestHelpers.hpp + ${CMAKE_CURRENT_LIST_DIR}/src/Test.hpp + ${CMAKE_CURRENT_LIST_DIR}/src/Test.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp ${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp diff --git a/tests/src/AccessGuard.cpp b/tests/src/AccessGuard.cpp index a0d1c6d31..56cbc727f 100644 --- a/tests/src/AccessGuard.cpp +++ b/tests/src/AccessGuard.cpp @@ -1,6 +1,6 @@ #include "common/UniqueAccess.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/BasicPubSub.cpp b/tests/src/BasicPubSub.cpp index dc2775220..6315970ff 100644 --- a/tests/src/BasicPubSub.cpp +++ b/tests/src/BasicPubSub.cpp @@ -1,7 +1,7 @@ #include "providers/liveupdates/BasicPubSubClient.hpp" #include "providers/liveupdates/BasicPubSubManager.hpp" +#include "Test.hpp" -#include #include #include #include diff --git a/tests/src/BttvLiveUpdates.cpp b/tests/src/BttvLiveUpdates.cpp index 580f2e61f..2d238f9b0 100644 --- a/tests/src/BttvLiveUpdates.cpp +++ b/tests/src/BttvLiveUpdates.cpp @@ -1,6 +1,7 @@ #include "providers/bttv/BttvLiveUpdates.hpp" -#include +#include "Test.hpp" + #include #include diff --git a/tests/src/ChannelChatters.cpp b/tests/src/ChannelChatters.cpp index c665836bb..79711ce15 100644 --- a/tests/src/ChannelChatters.cpp +++ b/tests/src/ChannelChatters.cpp @@ -1,8 +1,8 @@ #include "common/ChannelChatters.hpp" #include "mocks/Channel.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/ChatterSet.cpp b/tests/src/ChatterSet.cpp index ac5b81ee9..57a67a771 100644 --- a/tests/src/ChatterSet.cpp +++ b/tests/src/ChatterSet.cpp @@ -1,6 +1,7 @@ #include "common/ChatterSet.hpp" -#include +#include "Test.hpp" + #include TEST(ChatterSet, insert) diff --git a/tests/src/Emojis.cpp b/tests/src/Emojis.cpp index 141a64afb..42df110a4 100644 --- a/tests/src/Emojis.cpp +++ b/tests/src/Emojis.cpp @@ -1,9 +1,8 @@ #include "providers/emoji/Emojis.hpp" #include "common/Literals.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/ExponentialBackoff.cpp b/tests/src/ExponentialBackoff.cpp index 2a4259744..7099ea08a 100644 --- a/tests/src/ExponentialBackoff.cpp +++ b/tests/src/ExponentialBackoff.cpp @@ -1,6 +1,6 @@ #include "util/ExponentialBackoff.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index ff1b05902..89c0a510f 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -13,8 +13,8 @@ #include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Emotes.hpp" +#include "Test.hpp" -#include #include #include @@ -101,7 +101,7 @@ namespace chatterino::filters { std::ostream &operator<<(std::ostream &os, Type t) { - os << qUtf8Printable(typeToString(t)); + os << typeToString(t); return os; } @@ -138,8 +138,8 @@ TEST(Filters, Validity) auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); EXPECT_EQ(isValid, expected) - << "Filter::fromString( " << qUtf8Printable(input) - << " ) should be " << (expected ? "valid" : "invalid"); + << "Filter::fromString( " << input << " ) should be " + << (expected ? "valid" : "invalid"); } } @@ -168,15 +168,14 @@ TEST(Filters, TypeSynthesis) { auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); - ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input) - << " ) is invalid"; + ASSERT_TRUE(isValid) + << "Filter::fromString( " << input << " ) is invalid"; auto filter = std::move(std::get(filterResult)); T type = filter.returnType(); EXPECT_EQ(type, expected) - << "Filter{ " << qUtf8Printable(input) << " } has type " << type - << " instead of " << expected << ".\nDebug: " - << qUtf8Printable(filter.debugString(typingContext)); + << "Filter{ " << input << " } has type " << type << " instead of " + << expected << ".\nDebug: " << filter.debugString(typingContext); } } @@ -244,17 +243,16 @@ TEST(Filters, Evaluation) { auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); - ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input) - << " ) is invalid"; + ASSERT_TRUE(isValid) + << "Filter::fromString( " << input << " ) is invalid"; auto filter = std::move(std::get(filterResult)); auto result = filter.execute(contextMap); EXPECT_EQ(result, expected) - << "Filter{ " << qUtf8Printable(input) << " } evaluated to " - << qUtf8Printable(result.toString()) << " instead of " - << qUtf8Printable(expected.toString()) << ".\nDebug: " - << qUtf8Printable(filter.debugString(typingContext)); + << "Filter{ " << input << " } evaluated to " << result.toString() + << " instead of " << expected.toString() + << ".\nDebug: " << filter.debugString(typingContext); } } @@ -354,20 +352,17 @@ TEST_F(FiltersF, ExpressionDebug) { const auto filterResult = Filter::fromString(input); const auto *filter = std::get_if(&filterResult); - EXPECT_NE(filter, nullptr) - << "Filter::fromString(" << qUtf8Printable(input) - << ") did not build a proper filter"; + EXPECT_NE(filter, nullptr) << "Filter::fromString(" << input + << ") did not build a proper filter"; const auto actualDebugString = filter->debugString(typingContext); EXPECT_EQ(actualDebugString, debugString) - << "filter->debugString() on '" << qUtf8Printable(input) - << "' should be '" << qUtf8Printable(debugString) << "', but got '" - << qUtf8Printable(actualDebugString) << "'"; + << "filter->debugString() on '" << input << "' should be '" + << debugString << "', but got '" << actualDebugString << "'"; const auto actualFilterString = filter->filterString(); EXPECT_EQ(actualFilterString, filterString) - << "filter->filterString() on '" << qUtf8Printable(input) - << "' should be '" << qUtf8Printable(filterString) << "', but got '" - << qUtf8Printable(actualFilterString) << "'"; + << "filter->filterString() on '" << input << "' should be '" + << filterString << "', but got '" << actualFilterString << "'"; } } diff --git a/tests/src/FormatTime.cpp b/tests/src/FormatTime.cpp index bc15f44ef..6fe82ab9a 100644 --- a/tests/src/FormatTime.cpp +++ b/tests/src/FormatTime.cpp @@ -1,6 +1,6 @@ #include "util/FormatTime.hpp" -#include +#include "Test.hpp" #include @@ -62,8 +62,8 @@ TEST(FormatTime, Int) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << input - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } @@ -130,8 +130,8 @@ TEST(FormatTime, QString) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << qUtf8Printable(input) - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } @@ -202,7 +202,6 @@ TEST(FormatTime, chrono) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " did not match expected value " - << qUtf8Printable(expected); + << actual << " did not match expected value " << expected; } } diff --git a/tests/src/Helpers.cpp b/tests/src/Helpers.cpp index d6a74fec0..c615167cf 100644 --- a/tests/src/Helpers.cpp +++ b/tests/src/Helpers.cpp @@ -1,6 +1,6 @@ #include "util/Helpers.hpp" -#include +#include "Test.hpp" using namespace chatterino; using namespace _helpers_internal; @@ -275,8 +275,8 @@ TEST(Helpers, skipSpace) const auto actual = skipSpace(makeView(c.input), c.startIdx); EXPECT_EQ(actual, c.expected) - << actual << " (" << qUtf8Printable(c.input) - << ") did not match expected value " << c.expected; + << actual << " (" << c.input << ") did not match expected value " + << c.expected; } } @@ -418,14 +418,13 @@ TEST(Helpers, findUnitMultiplierToSec) if (c.expectedMultiplier == bad) { - EXPECT_FALSE(actual.second) << qUtf8Printable(c.input); + EXPECT_FALSE(actual.second) << c.input; } else { EXPECT_TRUE(pos == c.expectedEndPos && actual.second && actual.first == c.expectedMultiplier) - << qUtf8Printable(c.input) - << ": Expected(end: " << c.expectedEndPos + << c.input << ": Expected(end: " << c.expectedEndPos << ", mult: " << c.expectedMultiplier << ") Actual(end: " << pos << ", mult: " << actual.first << ")"; } @@ -503,7 +502,7 @@ TEST(Helpers, parseDurationToSeconds) const auto actual = parseDurationToSeconds(c.input, c.noUnitMultiplier); EXPECT_EQ(actual, c.output) - << actual << " (" << qUtf8Printable(c.input) - << ") did not match expected value " << c.output; + << actual << " (" << c.input << ") did not match expected value " + << c.output; } } diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index a45bbf98c..090acf37b 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -10,9 +10,8 @@ #include "providers/twitch/TwitchBadge.hpp" // for Badge #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include -#include #include #include #include @@ -216,11 +215,9 @@ protected: input.originalMessage, input.flags); EXPECT_EQ(isMatch, expected.state) - << qUtf8Printable(input.senderName) << ": " - << qUtf8Printable(input.originalMessage); + << input.senderName << ": " << input.originalMessage; EXPECT_EQ(matchResult, expected.result) - << qUtf8Printable(input.senderName) << ": " - << qUtf8Printable(input.originalMessage); + << input.senderName << ": " << input.originalMessage; } } diff --git a/tests/src/HighlightPhrase.cpp b/tests/src/HighlightPhrase.cpp index 374670b03..2ec2530f0 100644 --- a/tests/src/HighlightPhrase.cpp +++ b/tests/src/HighlightPhrase.cpp @@ -1,6 +1,6 @@ #include "controllers/highlights/HighlightPhrase.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/Hotkeys.cpp b/tests/src/Hotkeys.cpp index ebbfe5029..7c3d8d10f 100644 --- a/tests/src/Hotkeys.cpp +++ b/tests/src/Hotkeys.cpp @@ -1,6 +1,5 @@ #include "controllers/hotkeys/HotkeyHelpers.hpp" - -#include +#include "Test.hpp" #include diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp index 860035438..22c42b31c 100644 --- a/tests/src/InputCompletion.cpp +++ b/tests/src/InputCompletion.cpp @@ -12,9 +12,9 @@ #include "singletons/Emotes.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" #include "widgets/splits/InputCompletionPopup.hpp" -#include #include #include #include @@ -224,7 +224,7 @@ void containsRoughly(std::span span, std::set values) } } - ASSERT_TRUE(found) << qPrintable(v) << " was not found in the span"; + ASSERT_TRUE(found) << v << " was not found in the span"; } } diff --git a/tests/src/IrcHelpers.cpp b/tests/src/IrcHelpers.cpp index acae81c35..d210b14e0 100644 --- a/tests/src/IrcHelpers.cpp +++ b/tests/src/IrcHelpers.cpp @@ -1,6 +1,7 @@ #include "util/IrcHelpers.hpp" -#include +#include "Test.hpp" + #include #include #include @@ -55,7 +56,7 @@ TEST(IrcHelpers, ParseTagString) const auto actual = parseTagString(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << qUtf8Printable(input) - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } diff --git a/tests/src/LimitedQueue.cpp b/tests/src/LimitedQueue.cpp index 39a8bba86..0a94ea928 100644 --- a/tests/src/LimitedQueue.cpp +++ b/tests/src/LimitedQueue.cpp @@ -1,6 +1,6 @@ #include "messages/LimitedQueue.hpp" -#include +#include "Test.hpp" #include diff --git a/tests/src/LinkInfo.cpp b/tests/src/LinkInfo.cpp index 91f065035..a06a78c0f 100644 --- a/tests/src/LinkInfo.cpp +++ b/tests/src/LinkInfo.cpp @@ -2,8 +2,7 @@ #include "common/Literals.hpp" #include "SignalSpy.hpp" - -#include +#include "Test.hpp" using namespace chatterino; using namespace literals; diff --git a/tests/src/LinkParser.cpp b/tests/src/LinkParser.cpp index cce5c8c6c..9d964ce15 100644 --- a/tests/src/LinkParser.cpp +++ b/tests/src/LinkParser.cpp @@ -1,8 +1,7 @@ #include "common/LinkParser.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/Literals.cpp b/tests/src/Literals.cpp index 77607b739..17d459b14 100644 --- a/tests/src/Literals.cpp +++ b/tests/src/Literals.cpp @@ -1,6 +1,6 @@ #include "common/Literals.hpp" -#include +#include "Test.hpp" using namespace chatterino::literals; diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index 9ce0c7f21..ab9a294c9 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -10,8 +10,8 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/NetworkCommon.cpp b/tests/src/NetworkCommon.cpp index 481f951ae..9beab8da6 100644 --- a/tests/src/NetworkCommon.cpp +++ b/tests/src/NetworkCommon.cpp @@ -1,6 +1,6 @@ #include "common/network/NetworkCommon.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index 2f6b8102f..ca723481e 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -2,8 +2,8 @@ #include "common/network/NetworkManager.hpp" #include "common/network/NetworkResult.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/NetworkResult.cpp b/tests/src/NetworkResult.cpp index 72a2ca771..4bf2366a3 100644 --- a/tests/src/NetworkResult.cpp +++ b/tests/src/NetworkResult.cpp @@ -1,6 +1,6 @@ #include "common/network/NetworkResult.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/NotebookTab.cpp b/tests/src/NotebookTab.cpp index 2ac4903f4..36133b648 100644 --- a/tests/src/NotebookTab.cpp +++ b/tests/src/NotebookTab.cpp @@ -7,10 +7,9 @@ #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" +#include "Test.hpp" #include "widgets/Notebook.hpp" -#include -#include #include #include diff --git a/tests/src/QMagicEnum.cpp b/tests/src/QMagicEnum.cpp index 80c265efe..6778427fe 100644 --- a/tests/src/QMagicEnum.cpp +++ b/tests/src/QMagicEnum.cpp @@ -2,8 +2,7 @@ #include "common/FlagsEnum.hpp" #include "common/Literals.hpp" - -#include +#include "Test.hpp" using namespace chatterino; using namespace literals; diff --git a/tests/src/RatelimitBucket.cpp b/tests/src/RatelimitBucket.cpp index c92a42234..850f14c68 100644 --- a/tests/src/RatelimitBucket.cpp +++ b/tests/src/RatelimitBucket.cpp @@ -1,6 +1,7 @@ #include "util/RatelimitBucket.hpp" -#include +#include "Test.hpp" + #include #include #include diff --git a/tests/src/Selection.cpp b/tests/src/Selection.cpp index 1f1f4a621..a904b0766 100644 --- a/tests/src/Selection.cpp +++ b/tests/src/Selection.cpp @@ -1,6 +1,6 @@ #include "messages/Selection.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/SeventvEventAPI.cpp b/tests/src/SeventvEventAPI.cpp index 780f90bac..4e2d32281 100644 --- a/tests/src/SeventvEventAPI.cpp +++ b/tests/src/SeventvEventAPI.cpp @@ -3,8 +3,8 @@ #include "providers/seventv/eventapi/Client.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/SplitInput.cpp b/tests/src/SplitInput.cpp index ed092f94b..d84da8118 100644 --- a/tests/src/SplitInput.cpp +++ b/tests/src/SplitInput.cpp @@ -12,11 +12,10 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "Test.hpp" #include "widgets/Notebook.hpp" #include "widgets/splits/Split.hpp" -#include -#include #include #include @@ -110,9 +109,8 @@ TEST_P(SplitInputTest, Reply) auto reply = MessagePtr(message); this->input.setReply(reply); QString actual = this->input.getInputText(); - ASSERT_EQ(expected, actual) - << "Input text after setReply should be '" << qUtf8Printable(expected) - << "', but got '" << qUtf8Printable(actual) << "'"; + ASSERT_EQ(expected, actual) << "Input text after setReply should be '" + << expected << "', but got '" << actual << "'"; } INSTANTIATE_TEST_SUITE_P( diff --git a/tests/src/Test.cpp b/tests/src/Test.cpp new file mode 100644 index 000000000..5f245f5d7 --- /dev/null +++ b/tests/src/Test.cpp @@ -0,0 +1,42 @@ +#include "Test.hpp" + +#include +#include + +std::ostream &operator<<(std::ostream &os, QStringView str) +{ + os << str.toString().toStdString(); + return os; +} + +std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) +{ + os << std::string_view{bytes.data(), static_cast(bytes.size())}; + return os; +} + +std::ostream &operator<<(std::ostream &os, const QString &str) +{ + os << str.toStdString(); + return os; +} + +// The PrintTo overloads use UniversalPrint to print strings in quotes. +// Even though this uses testing::internal, this is publically documented in +// gtest/gtest-printers.h. + +void PrintTo(const QByteArray &bytes, std::ostream *os) +{ + ::testing::internal::UniversalPrint(bytes.toStdString(), os); +} + +void PrintTo(QStringView str, std::ostream *os) +{ + ::testing::internal::UniversalPrint( + std::u16string{str.utf16(), static_cast(str.size())}, os); +} + +void PrintTo(const QString &str, std::ostream *os) +{ + ::testing::internal::UniversalPrint(str.toStdU16String(), os); +} diff --git a/tests/src/Test.hpp b/tests/src/Test.hpp new file mode 100644 index 000000000..064f90c6d --- /dev/null +++ b/tests/src/Test.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include + +class QString; +class QStringView; +class QByteArray; + +// This file is included in all TUs in chatterino-test to avoid ODR violations. +std::ostream &operator<<(std::ostream &os, QStringView str); +std::ostream &operator<<(std::ostream &os, const QByteArray &bytes); +std::ostream &operator<<(std::ostream &os, const QString &str); + +// NOLINTBEGIN(readability-identifier-naming) +// PrintTo is used for naming parameterized tests, and is part of gtest +void PrintTo(const QByteArray &bytes, std::ostream *os); +void PrintTo(QStringView str, std::ostream *os); +void PrintTo(const QString &str, std::ostream *os); +// NOLINTEND(readability-identifier-naming) diff --git a/tests/src/TestHelpers.hpp b/tests/src/TestHelpers.hpp deleted file mode 100644 index 30a2d0b1a..000000000 --- a/tests/src/TestHelpers.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -template -class ReceivedMessage -{ - mutable std::mutex mutex; - - bool isSet{false}; - T t; - -public: - ReceivedMessage() = default; - - explicit operator bool() const - { - std::unique_lock lock(this->mutex); - - return this->isSet; - } - - ReceivedMessage &operator=(const T &newT) - { - std::unique_lock lock(this->mutex); - - this->isSet = true; - this->t = newT; - - return *this; - } - - bool operator==(const T &otherT) const - { - std::unique_lock lock(this->mutex); - - return this->t == otherT; - } - - const T *operator->() const - { - return &this->t; - } -}; - -inline std::ostream &operator<<(std::ostream &os, const QStringView &str) -{ - os << qUtf8Printable(str.toString()); - return os; -} - -inline std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) -{ - os << bytes.toStdString(); - return os; -} - -inline std::ostream &operator<<(std::ostream &os, const QString &str) -{ - os << qUtf8Printable(str); - return os; -} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index 77ddcdf46..d9d1d5a62 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -15,9 +15,8 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Emotes.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include #include diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index 30e02e567..728b0e5bb 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -4,12 +4,12 @@ #include "providers/twitch/pubsubmessages/AutoMod.hpp" #include "providers/twitch/pubsubmessages/Whisper.hpp" #include "providers/twitch/TwitchAccount.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include +#include #include using namespace chatterino; @@ -33,6 +33,47 @@ using namespace std::chrono_literals; #ifdef RUN_PUBSUB_TESTS +template +class ReceivedMessage +{ + mutable std::mutex mutex; + + bool isSet{false}; + T t; + +public: + ReceivedMessage() = default; + + explicit operator bool() const + { + std::unique_lock lock(this->mutex); + + return this->isSet; + } + + ReceivedMessage &operator=(const T &newT) + { + std::unique_lock lock(this->mutex); + + this->isSet = true; + this->t = newT; + + return *this; + } + + bool operator==(const T &otherT) const + { + std::unique_lock lock(this->mutex); + + return this->t == otherT; + } + + const T *operator->() const + { + return &this->t; + } +}; + class FTest : public PubSub { public: diff --git a/tests/src/Updates.cpp b/tests/src/Updates.cpp index da4762517..ce16f329f 100644 --- a/tests/src/Updates.cpp +++ b/tests/src/Updates.cpp @@ -1,8 +1,8 @@ #include "singletons/Updates.hpp" #include "common/Version.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/UtilTwitch.cpp b/tests/src/UtilTwitch.cpp index 6a0b58d9f..3a2a7b41b 100644 --- a/tests/src/UtilTwitch.cpp +++ b/tests/src/UtilTwitch.cpp @@ -1,6 +1,6 @@ +#include "Test.hpp" #include "util/Twitch.hpp" -#include #include #include #include @@ -72,9 +72,8 @@ TEST(UtilTwitch, StripUserName) stripUserName(userName); EXPECT_EQ(userName, expectedUserName) - << qUtf8Printable(userName) << " (" << qUtf8Printable(inputUserName) - << ") did not match expected value " - << qUtf8Printable(expectedUserName); + << userName << " (" << inputUserName + << ") did not match expected value " << expectedUserName; } } @@ -153,10 +152,8 @@ TEST(UtilTwitch, StripChannelName) stripChannelName(userName); EXPECT_EQ(userName, expectedChannelName) - << qUtf8Printable(userName) << " (" - << qUtf8Printable(inputChannelName) - << ") did not match expected value " - << qUtf8Printable(expectedChannelName); + << userName << " (" << inputChannelName + << ") did not match expected value " << expectedChannelName; } } @@ -259,14 +256,12 @@ TEST(UtilTwitch, ParseUserNameOrID) auto [actualUserName, actualUserID] = parseUserNameOrID(input); EXPECT_EQ(actualUserName, expectedUserName) - << "name " << qUtf8Printable(actualUserName) << " (" - << qUtf8Printable(input) << ") did not match expected value " - << qUtf8Printable(expectedUserName); + << "name " << actualUserName << " (" << input + << ") did not match expected value " << expectedUserName; EXPECT_EQ(actualUserID, expectedUserID) - << "id " << qUtf8Printable(actualUserID) << " (" - << qUtf8Printable(input) << ") did not match expected value " - << qUtf8Printable(expectedUserID); + << "id " << actualUserID << " (" << input + << ") did not match expected value " << expectedUserID; } } @@ -319,7 +314,7 @@ TEST(UtilTwitch, UserLoginRegexp) auto actual = regexp.match(inputUserLogin); EXPECT_EQ(match.hasMatch(), expectedMatch) - << qUtf8Printable(inputUserLogin) << " did not match as expected"; + << inputUserLogin << " did not match as expected"; } } @@ -371,7 +366,7 @@ TEST(UtilTwitch, UserNameRegexp) auto actual = regexp.match(inputUserLogin); EXPECT_EQ(match.hasMatch(), expectedMatch) - << qUtf8Printable(inputUserLogin) << " did not match as expected"; + << inputUserLogin << " did not match as expected"; } } @@ -405,8 +400,7 @@ TEST(UtilTwitch, CleanHelixColor) cleanHelixColorName(actualColor); EXPECT_EQ(actualColor, expectedColor) - << qUtf8Printable(inputColor) << " cleaned up to " - << qUtf8Printable(actualColor) << " instead of " - << qUtf8Printable(expectedColor); + << inputColor << " cleaned up to " << actualColor << " instead of " + << expectedColor; } } diff --git a/tests/src/XDGDesktopFile.cpp b/tests/src/XDGDesktopFile.cpp index bffe529aa..69f4d3706 100644 --- a/tests/src/XDGDesktopFile.cpp +++ b/tests/src/XDGDesktopFile.cpp @@ -1,6 +1,7 @@ #include "util/XDGDesktopFile.hpp" -#include +#include "Test.hpp" + #include #if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) diff --git a/tests/src/XDGHelper.cpp b/tests/src/XDGHelper.cpp index 3926d21d9..3ab48daa3 100644 --- a/tests/src/XDGHelper.cpp +++ b/tests/src/XDGHelper.cpp @@ -1,8 +1,7 @@ #include "util/XDGHelper.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 3b24a9978..6c82f632c 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,8 +1,8 @@ #include "common/network/NetworkManager.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include #include #include #include From 56fa973d7c1d8bb895b69e83942a9dd2ef6ba0cb Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 5 May 2024 19:37:22 +0200 Subject: [PATCH 62/71] fix: prefer reporting error over status for 200 OK (#5378) --- CHANGELOG.md | 1 + src/common/network/NetworkResult.cpp | 11 ++++++++++- tests/src/NetworkResult.cpp | 13 +++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d2b0054..eae3d6f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/common/network/NetworkResult.cpp b/src/common/network/NetworkResult.cpp index 177d2ae6f..c6544eaad 100644 --- a/src/common/network/NetworkResult.cpp +++ b/src/common/network/NetworkResult.cpp @@ -67,7 +67,9 @@ const QByteArray &NetworkResult::getData() const QString NetworkResult::formatError() const { - if (this->status_) + // Print the status for errors that mirror HTTP status codes (=0 || >99) + if (this->status_ && (this->error_ == QNetworkReply::NoError || + this->error_ > QNetworkReply::UnknownNetworkError)) { return QString::number(*this->status_); } @@ -77,6 +79,13 @@ QString NetworkResult::formatError() const this->error_); if (name == nullptr) { + if (this->status_) + { + return QStringLiteral("unknown error (status: %1, error: %2)") + .arg(QString::number(*this->status_), + QString::number(this->error_)); + } + return QStringLiteral("unknown error (%1)").arg(this->error_); } return name; diff --git a/tests/src/NetworkResult.cpp b/tests/src/NetworkResult.cpp index 4bf2366a3..4d4c57421 100644 --- a/tests/src/NetworkResult.cpp +++ b/tests/src/NetworkResult.cpp @@ -37,12 +37,21 @@ TEST(NetworkResult, Errors) "RemoteHostClosedError"); // status code takes precedence - checkResult({Error::TimeoutError, 400, {}}, Error::TimeoutError, 400, - "400"); + checkResult({Error::InternalServerError, 400, {}}, + Error::InternalServerError, 400, "400"); + + // error takes precedence (1..=99) + checkResult({Error::BackgroundRequestNotAllowedError, 400, {}}, + Error::BackgroundRequestNotAllowedError, 400, + "BackgroundRequestNotAllowedError"); + checkResult({Error::UnknownNetworkError, 400, {}}, + Error::UnknownNetworkError, 400, "UnknownNetworkError"); } TEST(NetworkResult, InvalidError) { checkResult({static_cast(-1), {}, {}}, static_cast(-1), std::nullopt, "unknown error (-1)"); + checkResult({static_cast(-1), 42, {}}, static_cast(-1), 42, + "unknown error (status: 42, error: -1)"); } From a43c4f371b3bbacd16a5a59da71c48b6f347d190 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 21:13:02 -0400 Subject: [PATCH 63/71] chore(deps): bump lib/settings from `70fbc72` to `03e8af1` (#5382) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index 70fbc7236..03e8af193 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 70fbc7236aa8bcf5db4748e7f56dad132d6fd402 +Subproject commit 03e8af1934e6151edfe8a44dfb025b747a31acdf From 321d881bfe2f7cc77ac64fc843484d5e9f170c43 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 6 May 2024 17:03:17 +0200 Subject: [PATCH 64/71] Release plugins alpha (#5288) --- .github/workflows/build.yml | 6 +++--- BUILDING_ON_FREEBSD.md | 2 +- BUILDING_ON_LINUX.md | 2 +- BUILDING_ON_MAC.md | 2 +- BUILDING_ON_WINDOWS.md | 1 + BUILDING_ON_WINDOWS_WITH_VCPKG.md | 1 + CHANGELOG.md | 1 + CMakeLists.txt | 2 +- src/widgets/settingspages/PluginsPage.cpp | 15 +++++++++++++++ 9 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e42b5dd6..5a4d07717 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,21 +115,21 @@ jobs: - os: macos-13 qt-version: 5.15.2 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: false # Windows - os: windows-latest qt-version: 6.5.0 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: false # Windows 7/8 - os: windows-latest qt-version: 5.15.2 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: true diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md index 8a1deeeba..26e751c93 100644 --- a/BUILDING_ON_FREEBSD.md +++ b/BUILDING_ON_FREEBSD.md @@ -15,7 +15,7 @@ FreeBSD 13.0-CURRENT. mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh cmake .. ``` diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index b901e8e6d..3aa2df4a8 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -51,7 +51,7 @@ nix-shell -p openssl boost qt6.full pkg-config cmake mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh cmake -DBUILD_WITH_QT6=ON -DBUILD_WITH_QTKEYCHAIN=OFF .. ``` diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index 78f94c7e9..1c8fc38a1 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -20,7 +20,7 @@ Local dev machines for testing are available on Apple Silicon on macOS 13. 1. Go to the project directory where you cloned Chatterino2 & its submodules 1. Create a build directory and go into it: `mkdir build && cd build` -1. Run CMake: +1. Run CMake. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. `cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..` 1. Build: `make` diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index dc66d65c4..42d71cc51 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -118,6 +118,7 @@ nmake ``` To build a debug build, you'll also need to add the `-s compiler.runtime_type=Debug` flag to the `conan install` invocation. See [this StackOverflow post](https://stackoverflow.com/questions/59828611/windeployqt-doesnt-deploy-qwindowsd-dll-for-a-debug-application/75607313#75607313) +To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake` command. #### Deploying Qt libraries diff --git a/BUILDING_ON_WINDOWS_WITH_VCPKG.md b/BUILDING_ON_WINDOWS_WITH_VCPKG.md index b99809431..ec9615724 100644 --- a/BUILDING_ON_WINDOWS_WITH_VCPKG.md +++ b/BUILDING_ON_WINDOWS_WITH_VCPKG.md @@ -50,4 +50,5 @@ This will require more than 30GB of free space on your hard drive. cmake --build . --parallel --config Release ``` When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain. + To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake -B build` command. 1. Run `.\bin\chatterino2.exe` diff --git a/CHANGELOG.md b/CHANGELOG.md index eae3d6f30..4bd5f9a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Major: Release plugins alpha. (#5288) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/CMakeLists.txt b/CMakeLists.txt index eec6b7e1c..6fb323286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF) option(BUILD_TRANSLATIONS "" OFF) option(BUILD_SHARED_LIBS "" OFF) option(CHATTERINO_LTO "Enable LTO for all targets" OFF) -option(CHATTERINO_PLUGINS "Enable EXPERIMENTAL plugin support in Chatterino" OFF) +option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" OFF) option(CHATTERINO_UPDATER "Enable update checks" ON) mark_as_advanced(CHATTERINO_UPDATER) diff --git a/src/widgets/settingspages/PluginsPage.cpp b/src/widgets/settingspages/PluginsPage.cpp index aad35f751..05c80a37c 100644 --- a/src/widgets/settingspages/PluginsPage.cpp +++ b/src/widgets/settingspages/PluginsPage.cpp @@ -37,6 +37,21 @@ PluginsPage::PluginsPage() auto group = layout.emplace("General plugin settings"); this->generalGroup = group.getElement(); auto groupLayout = group.setLayoutType(); + auto *scaryLabel = new QLabel( + "Plugins can expand functionality of " + "Chatterino. They can be made in Lua. This functionality is " + "still in public alpha stage. Use ONLY the plugins you trust. " + "The permission system is best effort, always " + "assume plugins can bypass permissions and can execute " + "arbitrary code. To see how to create plugins " + + formatRichNamedLink("https://github.com/Chatterino/chatterino2/" + "blob/master/docs/wip-plugins.md", + "look at the manual") + + "."); + scaryLabel->setWordWrap(true); + scaryLabel->setOpenExternalLinks(true); + groupLayout->addRow(scaryLabel); + auto *description = new QLabel("You can load plugins by putting them into " + formatRichNamedLink( From c3b84cb4b6f4b9354195169a78d4d6057373efa5 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 11 May 2024 12:54:27 +0200 Subject: [PATCH 65/71] Add custom image functionality for inline mod buttons. (#5369) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/CMakeLists.txt | 8 +- .../moderationactions/ModerationAction.cpp | 79 ++++++------ .../moderationactions/ModerationAction.hpp | 42 ++++++- .../ModerationActionModel.cpp | 29 ++++- .../ModerationActionModel.hpp | 5 + src/providers/twitch/TwitchBadges.cpp | 49 ++------ src/providers/twitch/TwitchBadges.hpp | 2 +- src/util/LoadPixmap.cpp | 48 ++++++++ src/util/LoadPixmap.hpp | 15 +++ src/widgets/helper/IconDelegate.cpp | 29 +++++ src/widgets/helper/IconDelegate.hpp | 19 +++ src/widgets/settingspages/ModerationPage.cpp | 46 ++++++- tests/CMakeLists.txt | 1 + tests/src/ModerationAction.cpp | 112 ++++++++++++++++++ 15 files changed, 396 insertions(+), 89 deletions(-) create mode 100644 src/util/LoadPixmap.cpp create mode 100644 src/util/LoadPixmap.hpp create mode 100644 src/widgets/helper/IconDelegate.cpp create mode 100644 src/widgets/helper/IconDelegate.hpp create mode 100644 tests/src/ModerationAction.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd5f9a47..ea438bd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Major: Release plugins alpha. (#5288) +- Minor: Add option to customise Moderation buttons with images. (#5369) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 301808906..15806aae1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -500,6 +500,8 @@ set(SOURCE_FILES util/IpcQueue.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp + util/LoadPixmap.cpp + util/LoadPixmap.hpp util/RapidjsonHelpers.cpp util/RapidjsonHelpers.hpp util/RatelimitBucket.cpp @@ -631,6 +633,8 @@ set(SOURCE_FILES widgets/helper/EditableModelView.hpp widgets/helper/EffectLabel.cpp widgets/helper/EffectLabel.hpp + widgets/helper/IconDelegate.cpp + widgets/helper/IconDelegate.hpp widgets/helper/InvisibleSizeGrip.cpp widgets/helper/InvisibleSizeGrip.hpp widgets/helper/NotebookButton.cpp @@ -639,8 +643,6 @@ set(SOURCE_FILES widgets/helper/NotebookTab.hpp widgets/helper/RegExpItemDelegate.cpp widgets/helper/RegExpItemDelegate.hpp - widgets/helper/TrimRegExpValidator.cpp - widgets/helper/TrimRegExpValidator.hpp widgets/helper/ResizingTextEdit.cpp widgets/helper/ResizingTextEdit.hpp widgets/helper/ScrollbarHighlight.cpp @@ -655,6 +657,8 @@ set(SOURCE_FILES widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButtons.cpp widgets/helper/TitlebarButtons.hpp + widgets/helper/TrimRegExpValidator.cpp + widgets/helper/TrimRegExpValidator.hpp widgets/layout/FlowLayout.cpp widgets/layout/FlowLayout.hpp diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index 2b3a95b06..a82d1848c 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -6,28 +6,11 @@ #include "singletons/Resources.hpp" #include +#include namespace chatterino { -// ModerationAction::ModerationAction(Image *_image, const QString &_action) -// : _isImage(true) -// , image(_image) -// , action(_action) -//{ -//} - -// ModerationAction::ModerationAction(const QString &_line1, const QString -// &_line2, -// const QString &_action) -// : _isImage(false) -// , image(nullptr) -// , line1(_line1) -// , line2(_line2) -// , action(_action) -//{ -//} - -ModerationAction::ModerationAction(const QString &action) +ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath) : action_(action) { static QRegularExpression replaceRegex("[!/.]"); @@ -37,6 +20,8 @@ ModerationAction::ModerationAction(const QString &action) if (timeoutMatch.hasMatch()) { + this->type_ = Type::Timeout; + // if (multipleTimeouts > 1) { // QString line1; // QString line2; @@ -99,24 +84,19 @@ ModerationAction::ModerationAction(const QString &action) } this->line2_ = "w"; } - - // line1 = this->line1_; - // line2 = this->line2_; - // } else { - // this->_moderationActions.emplace_back(getResources().buttonTimeout, - // str); - // } } else if (action.startsWith("/ban ")) { - this->imageToLoad_ = 1; + this->type_ = Type::Ban; } else if (action.startsWith("/delete ")) { - this->imageToLoad_ = 2; + this->type_ = Type::Delete; } else { + this->type_ = Type::Custom; + QString xD = action; xD.replace(replaceRegex, ""); @@ -124,6 +104,11 @@ ModerationAction::ModerationAction(const QString &action) this->line1_ = xD.mid(0, 2); this->line2_ = xD.mid(2, 2); } + + if (iconPath.isValid()) + { + this->iconPath_ = iconPath; + } } bool ModerationAction::operator==(const ModerationAction &other) const @@ -139,19 +124,23 @@ bool ModerationAction::isImage() const const std::optional &ModerationAction::getImage() const { assertInGuiThread(); - - if (this->imageToLoad_ != 0) + if (this->image_.has_value()) { - if (this->imageToLoad_ == 1) - { - this->image_ = - Image::fromResourcePixmap(getResources().buttons.ban); - } - else if (this->imageToLoad_ == 2) - { - this->image_ = - Image::fromResourcePixmap(getResources().buttons.trashCan); - } + return this->image_; + } + + if (this->iconPath_.isValid()) + { + this->image_ = Image::fromUrl({this->iconPath_.toString()}); + } + else if (this->type_ == Type::Ban) + { + this->image_ = Image::fromResourcePixmap(getResources().buttons.ban); + } + else if (this->type_ == Type::Delete) + { + this->image_ = + Image::fromResourcePixmap(getResources().buttons.trashCan); } return this->image_; @@ -172,4 +161,14 @@ const QString &ModerationAction::getAction() const return this->action_; } +const QUrl &ModerationAction::iconPath() const +{ + return this->iconPath_; +} + +ModerationAction::Type ModerationAction::getType() const +{ + return this->type_; +} + } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index 8fa4c9be8..643eaf06d 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -16,7 +17,32 @@ using ImagePtr = std::shared_ptr; class ModerationAction { public: - ModerationAction(const QString &action); + /** + * Type of the action, parsed from the input `action` + */ + enum class Type { + /** + * /ban + */ + Ban, + + /** + * /delete + */ + Delete, + + /** + * /timeout + */ + Timeout, + + /** + * Anything not matching the action types above + */ + Custom, + }; + + ModerationAction(const QString &action, const QUrl &iconPath = {}); bool operator==(const ModerationAction &other) const; @@ -25,13 +51,18 @@ public: const QString &getLine1() const; const QString &getLine2() const; const QString &getAction() const; + const QUrl &iconPath() const; + Type getType() const; private: mutable std::optional image_; QString line1_; QString line2_; QString action_; - int imageToLoad_{}; + + Type type_{}; + + QUrl iconPath_; }; } // namespace chatterino @@ -46,6 +77,7 @@ struct Serialize { rapidjson::Value ret(rapidjson::kObjectType); chatterino::rj::set(ret, "pattern", value.getAction(), a); + chatterino::rj::set(ret, "icon", value.iconPath().toString(), a); return ret; } @@ -63,10 +95,12 @@ struct Deserialize { } QString pattern; - chatterino::rj::getSafe(value, "pattern", pattern); - return chatterino::ModerationAction(pattern); + QString icon; + chatterino::rj::getSafe(value, "icon", icon); + + return chatterino::ModerationAction(pattern, QUrl(icon)); } }; diff --git a/src/controllers/moderationactions/ModerationActionModel.cpp b/src/controllers/moderationactions/ModerationActionModel.cpp index d6595556d..f7160b589 100644 --- a/src/controllers/moderationactions/ModerationActionModel.cpp +++ b/src/controllers/moderationactions/ModerationActionModel.cpp @@ -1,13 +1,19 @@ #include "controllers/moderationactions/ModerationActionModel.hpp" #include "controllers/moderationactions/ModerationAction.hpp" +#include "messages/Image.hpp" +#include "util/LoadPixmap.hpp" +#include "util/PostToThread.hpp" #include "util/StandardItemHelper.hpp" +#include +#include + namespace chatterino { // commandmodel ModerationActionModel ::ModerationActionModel(QObject *parent) - : SignalVectorModel(1, parent) + : SignalVectorModel(2, parent) { } @@ -15,14 +21,31 @@ ModerationActionModel ::ModerationActionModel(QObject *parent) ModerationAction ModerationActionModel::getItemFromRow( std::vector &row, const ModerationAction &original) { - return ModerationAction(row[0]->data(Qt::DisplayRole).toString()); + return ModerationAction( + row[Column::Command]->data(Qt::DisplayRole).toString(), + row[Column::Icon]->data(Qt::UserRole).toString()); } // turns a row in the model into a vector item void ModerationActionModel::getRowFromItem(const ModerationAction &item, std::vector &row) { - setStringItem(row[0], item.getAction()); + setStringItem(row[Column::Command], item.getAction()); + setFilePathItem(row[Column::Icon], item.iconPath()); + if (!item.iconPath().isEmpty()) + { + auto oImage = item.getImage(); + assert(oImage.has_value()); + if (oImage.has_value()) + { + auto url = oImage->get()->url(); + loadPixmapFromUrl(url, [row](const QPixmap &pixmap) { + postToThread([row, pixmap]() { + row[Column::Icon]->setData(pixmap, Qt::DecorationRole); + }); + }); + } + } } } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp index e8e51db03..3382b4378 100644 --- a/src/controllers/moderationactions/ModerationActionModel.hpp +++ b/src/controllers/moderationactions/ModerationActionModel.hpp @@ -13,6 +13,11 @@ class ModerationActionModel : public SignalVectorModel public: explicit ModerationActionModel(QObject *parent); + enum Column { + Command = 0, + Icon = 1, + }; + protected: // turn a vector item into a model row ModerationAction getItemFromRow(std::vector &row, diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index 14c7475e0..6e2b4c4aa 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -7,6 +7,7 @@ #include "messages/Image.hpp" #include "providers/twitch/api/Helix.hpp" #include "util/DisplayBadge.hpp" +#include "util/LoadPixmap.hpp" #include #include @@ -239,48 +240,20 @@ void TwitchBadges::getBadgeIcons(const QList &badges, } } -void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image, +void TwitchBadges::loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback) { - auto url = image->url().string; - NetworkRequest(url) - .concurrent() - .cache() - .onSuccess([this, name, callback, url](auto result) { - auto data = result.getData(); + loadPixmapFromUrl(image->url(), + [this, name, callback{std::move(callback)}](auto pixmap) { + auto icon = std::make_shared(pixmap); - // const cast since we are only reading from it - QBuffer buffer(const_cast(&data)); - buffer.open(QIODevice::ReadOnly); - QImageReader reader(&buffer); + { + std::unique_lock lock(this->badgesMutex_); + this->badgesMap_[name] = icon; + } - if (!reader.canRead() || reader.size().isEmpty()) - { - qCWarning(chatterinoTwitch) - << "Can't read badge image at" << url << "for" << name - << reader.errorString(); - return; - } - - QImage image = reader.read(); - if (image.isNull()) - { - qCWarning(chatterinoTwitch) - << "Failed reading badge image at" << url << "for" << name - << reader.errorString(); - return; - } - - auto icon = std::make_shared(QPixmap::fromImage(image)); - - { - std::unique_lock lock(this->badgesMutex_); - this->badgesMap_[name] = icon; - } - - callback(name, icon); - }) - .execute(); + callback(name, icon); + }); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp index 9964030f0..fff0f5aff 100644 --- a/src/providers/twitch/TwitchBadges.hpp +++ b/src/providers/twitch/TwitchBadges.hpp @@ -48,7 +48,7 @@ public: private: void parseTwitchBadges(QJsonObject root); void loaded(); - void loadEmoteImage(const QString &name, ImagePtr image, + void loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback); std::shared_mutex badgesMutex_; diff --git a/src/util/LoadPixmap.cpp b/src/util/LoadPixmap.cpp new file mode 100644 index 000000000..99fdf95f3 --- /dev/null +++ b/src/util/LoadPixmap.cpp @@ -0,0 +1,48 @@ +#include "util/LoadPixmap.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include +#include + +namespace chatterino { + +void loadPixmapFromUrl(const Url &url, std::function &&callback) +{ + NetworkRequest(url.string) + .concurrent() + .cache() + .onSuccess( + [callback = std::move(callback), url](const NetworkResult &result) { + auto data = result.getData(); + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + QImageReader reader(&buffer); + + if (!reader.canRead() || reader.size().isEmpty()) + { + qCWarning(chatterinoImage) + << "Can't read image file at" << url.string << ":" + << reader.errorString(); + return; + } + + QImage image = reader.read(); + if (image.isNull()) + { + qCWarning(chatterinoImage) + << "Failed reading image at" << url.string << ":" + << reader.errorString(); + return; + } + + callback(QPixmap::fromImage(image)); + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/util/LoadPixmap.hpp b/src/util/LoadPixmap.hpp new file mode 100644 index 000000000..81fb11921 --- /dev/null +++ b/src/util/LoadPixmap.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "common/Aliases.hpp" + +#include + +namespace chatterino { + +/** + * Loads an image from url into a QPixmap. Allows for file:// protocol links. Uses cacheing. + * + * @param callback The callback you will get the pixmap by. It will be invoked concurrently with no guarantees on which thread. + */ +void loadPixmapFromUrl(const Url &url, std::function &&callback); + +} // namespace chatterino diff --git a/src/widgets/helper/IconDelegate.cpp b/src/widgets/helper/IconDelegate.cpp new file mode 100644 index 000000000..c89037eea --- /dev/null +++ b/src/widgets/helper/IconDelegate.cpp @@ -0,0 +1,29 @@ +#include "widgets/helper/IconDelegate.hpp" + +#include +#include + +namespace chatterino { + +IconDelegate::IconDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto data = index.data(Qt::DecorationRole); + + if (data.type() != QVariant::Pixmap) + { + return QStyledItemDelegate::paint(painter, option, index); + } + + auto scaledRect = option.rect; + scaledRect.setWidth(scaledRect.height()); + + painter->drawPixmap(scaledRect, data.value()); +} + +} // namespace chatterino diff --git a/src/widgets/helper/IconDelegate.hpp b/src/widgets/helper/IconDelegate.hpp new file mode 100644 index 000000000..6afd5183a --- /dev/null +++ b/src/widgets/helper/IconDelegate.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace chatterino { + +/** + * IconDelegate draws the decoration role pixmap scaled down to a square icon + */ +class IconDelegate : public QStyledItemDelegate +{ +public: + explicit IconDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/ModerationPage.cpp b/src/widgets/settingspages/ModerationPage.cpp index fce69eff0..65ba577b1 100644 --- a/src/widgets/settingspages/ModerationPage.cpp +++ b/src/widgets/settingspages/ModerationPage.cpp @@ -9,12 +9,16 @@ #include "singletons/Settings.hpp" #include "util/Helpers.hpp" #include "util/LayoutCreator.hpp" +#include "util/LoadPixmap.hpp" +#include "util/PostToThread.hpp" #include "widgets/helper/EditableModelView.hpp" +#include "widgets/helper/IconDelegate.hpp" #include #include #include #include +#include #include #include #include @@ -207,11 +211,51 @@ ModerationPage::ModerationPage() ->initialized(&getSettings()->moderationActions)) .getElement(); - view->setTitles({"Actions"}); + view->setTitles({"Action", "Icon"}); view->getTableView()->horizontalHeader()->setSectionResizeMode( QHeaderView::Fixed); view->getTableView()->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); + view->getTableView()->setItemDelegateForColumn( + ModerationActionModel::Column::Icon, new IconDelegate(view)); + QObject::connect( + view->getTableView(), &QTableView::clicked, + [this, view](const QModelIndex &clicked) { + if (clicked.column() == ModerationActionModel::Column::Icon) + { + auto fileUrl = QFileDialog::getOpenFileUrl( + this, "Open Image", QUrl(), + "Image Files (*.png *.jpg *.jpeg)"); + view->getModel()->setData(clicked, fileUrl, Qt::UserRole); + view->getModel()->setData(clicked, fileUrl.fileName(), + Qt::DisplayRole); + // Clear the icon if the user canceled the dialog + if (fileUrl.isEmpty()) + { + view->getModel()->setData(clicked, QVariant(), + Qt::DecorationRole); + } + else + { + // QPointer will be cleared when view is destroyed + QPointer viewtemp = view; + + loadPixmapFromUrl( + {fileUrl.toString()}, + [clicked, view = viewtemp](const QPixmap &pixmap) { + postToThread([clicked, view, pixmap]() { + if (view.isNull()) + { + return; + } + + view->getModel()->setData( + clicked, pixmap, Qt::DecorationRole); + }); + }); + } + } + }); // We can safely ignore this signal connection since we own the view std::ignore = view->addButtonPressed.connect([] { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bd35b79de..8ea086b13 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp # Add your new file above this line! ) diff --git a/tests/src/ModerationAction.cpp b/tests/src/ModerationAction.cpp new file mode 100644 index 000000000..ce32fb39d --- /dev/null +++ b/tests/src/ModerationAction.cpp @@ -0,0 +1,112 @@ +#include "controllers/moderationactions/ModerationAction.hpp" + +#include "messages/Image.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" + +#include +#include + +using namespace chatterino; + +using namespace std::chrono_literals; + +namespace { + +class MockApplication : mock::EmptyApplication +{ +public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + { + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + Settings settings; + Emotes emotes; +}; + +class ModerationActionTest : public ::testing::Test +{ +public: + MockApplication mockApplication; +}; + +} // namespace + +TEST_F(ModerationActionTest, Parse) +{ + struct TestCase { + QString action; + QString iconPath; + + QString expectedLine1; + QString expectedLine2; + + std::optional expectedImage; + + ModerationAction::Type expectedType; + }; + + std::vector tests{ + { + .action = "/ban forsen", + .expectedImage = + Image::fromResourcePixmap(getResources().buttons.ban), + .expectedType = ModerationAction::Type::Ban, + }, + { + .action = "/delete {message.id}", + .expectedImage = + Image::fromResourcePixmap(getResources().buttons.trashCan), + .expectedType = ModerationAction::Type::Delete, + }, + { + .action = "/timeout {user.name} 1d", + .expectedLine1 = "1", + .expectedLine2 = "d", + .expectedType = ModerationAction::Type::Timeout, + }, + { + .action = ".timeout {user.name} 300", + .expectedLine1 = "5", + .expectedLine2 = "m", + .expectedType = ModerationAction::Type::Timeout, + }, + { + .action = "forsen", + .expectedLine1 = "fo", + .expectedLine2 = "rs", + .expectedType = ModerationAction::Type::Custom, + }, + { + .action = "forsen", + .iconPath = "file:///this-is-the-path-to-the-icon.png", + .expectedLine1 = "fo", + .expectedLine2 = "rs", + .expectedImage = + Image::fromUrl(Url{"file:///this-is-the-path-to-the-icon.png"}), + .expectedType = ModerationAction::Type::Custom, + }, + }; + + for (const auto &test : tests) + { + ModerationAction moderationAction(test.action, test.iconPath); + + EXPECT_EQ(moderationAction.getAction(), test.action); + + EXPECT_EQ(moderationAction.getLine1(), test.expectedLine1); + EXPECT_EQ(moderationAction.getLine2(), test.expectedLine2); + + EXPECT_EQ(moderationAction.getImage(), test.expectedImage); + + EXPECT_EQ(moderationAction.getType(), test.expectedType); + } +} From 5c539ebe9a735757541d72b5d06a50d810baea6e Mon Sep 17 00:00:00 2001 From: Brian <18603393+brian6932@users.noreply.github.com> Date: Sat, 11 May 2024 12:52:25 -0400 Subject: [PATCH 66/71] fix: Missing includes when building with `USE_PRECOMPILED_HEADERS=OFF` (#5389) --- src/util/AttachToConsole.cpp | 1 + src/widgets/helper/TitlebarButtons.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/util/AttachToConsole.cpp b/src/util/AttachToConsole.cpp index 41689c699..5f887260e 100644 --- a/src/util/AttachToConsole.cpp +++ b/src/util/AttachToConsole.cpp @@ -3,6 +3,7 @@ #ifdef USEWINSDK # include +# include # include #endif diff --git a/src/widgets/helper/TitlebarButtons.hpp b/src/widgets/helper/TitlebarButtons.hpp index 42a430d69..e7ee3eb5b 100644 --- a/src/widgets/helper/TitlebarButtons.hpp +++ b/src/widgets/helper/TitlebarButtons.hpp @@ -3,6 +3,7 @@ class QPoint; class QWidget; +#include #include namespace chatterino { From 8202cd0d9969fb88becb8aeaac607fc56697b541 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 12:52:58 +0200 Subject: [PATCH 67/71] refactor: cleanup and document `Scrollbar` (#5334) Co-authored-by: Rasmus Karlsson Co-authored-by: Daniel Sage --- CHANGELOG.md | 1 + src/widgets/Scrollbar.cpp | 325 ++++++++++------------- src/widgets/Scrollbar.hpp | 140 ++++++++-- src/widgets/dialogs/EmotePopup.cpp | 4 +- src/widgets/dialogs/ReplyThreadPopup.cpp | 4 +- src/widgets/dialogs/UserInfoPopup.cpp | 4 +- src/widgets/helper/ChannelView.cpp | 6 +- src/widgets/splits/Split.cpp | 4 +- tests/CMakeLists.txt | 1 + tests/src/ModerationAction.cpp | 2 +- tests/src/Scrollbar.cpp | 187 +++++++++++++ 11 files changed, 453 insertions(+), 225 deletions(-) create mode 100644 tests/src/Scrollbar.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ea438bd61..631b3eb1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) +- Dev: Refactor and document `Scrollbar`. (#5334) ## 2.5.1 diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 6ca0e248e..e1492b673 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -4,7 +4,6 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" -#include "util/Clamp.hpp" #include "widgets/helper/ChannelView.hpp" #include @@ -13,7 +12,19 @@ #include -#define MIN_THUMB_HEIGHT 10 +namespace { + +constexpr int MIN_THUMB_HEIGHT = 10; + +/// Amount of messages to move by when clicking on the track +constexpr qreal SCROLL_DELTA = 5.0; + +bool areClose(auto a, auto b) +{ + return std::abs(a - b) <= 0.0001; +} + +} // namespace namespace chatterino { @@ -22,40 +33,51 @@ Scrollbar::Scrollbar(size_t messagesLimit, ChannelView *parent) , currentValueAnimation_(this, "currentValue_") , highlights_(messagesLimit) { - this->resize(int(16 * this->scale()), 100); + this->resize(static_cast(16 * this->scale()), 100); this->currentValueAnimation_.setDuration(150); this->currentValueAnimation_.setEasingCurve( QEasingCurve(QEasingCurve::OutCubic)); connect(&this->currentValueAnimation_, &QAbstractAnimation::finished, this, - &Scrollbar::resetMaximum); + &Scrollbar::resetBounds); this->setMouseTracking(true); } +boost::circular_buffer Scrollbar::getHighlights() const +{ + return this->highlights_; +} + void Scrollbar::addHighlight(ScrollbarHighlight highlight) { - this->highlights_.pushBack(highlight); + this->highlights_.push_back(std::move(highlight)); } void Scrollbar::addHighlightsAtStart( - const std::vector &_highlights) + const std::vector &highlights) { - this->highlights_.pushFront(_highlights); + size_t nItems = std::min(highlights.size(), this->highlights_.capacity() - + this->highlights_.size()); + + if (nItems == 0) + { + return; + } + + for (size_t i = 0; i < nItems; i++) + { + this->highlights_.push_front(highlights[highlights.size() - 1 - i]); + } } void Scrollbar::replaceHighlight(size_t index, ScrollbarHighlight replacement) { - this->highlights_.replaceItem(index, replacement); -} + if (this->highlights_.size() <= index) + { + return; + } -void Scrollbar::pauseHighlights() -{ - this->highlightsPaused_ = true; -} - -void Scrollbar::unpauseHighlights() -{ - this->highlightsPaused_ = false; + this->highlights_[index] = std::move(replacement); } void Scrollbar::clearHighlights() @@ -63,16 +85,6 @@ void Scrollbar::clearHighlights() this->highlights_.clear(); } -LimitedQueueSnapshot &Scrollbar::getHighlightSnapshot() -{ - if (!this->highlightsPaused_) - { - this->highlightSnapshot_ = this->highlights_.getSnapshot(); - } - - return this->highlightSnapshot_; -} - void Scrollbar::scrollToBottom(bool animate) { this->setDesiredValue(this->getBottom(), animate); @@ -102,7 +114,7 @@ void Scrollbar::offsetMaximum(qreal value) this->updateScroll(); } -void Scrollbar::resetMaximum() +void Scrollbar::resetBounds() { if (this->minimum_ > 0) { @@ -132,26 +144,19 @@ void Scrollbar::offsetMinimum(qreal value) this->updateScroll(); } -void Scrollbar::setLargeChange(qreal value) +void Scrollbar::setPageSize(qreal value) { - this->largeChange_ = value; - - this->updateScroll(); -} - -void Scrollbar::setSmallChange(qreal value) -{ - this->smallChange_ = value; + this->pageSize_ = value; this->updateScroll(); } void Scrollbar::setDesiredValue(qreal value, bool animated) { - value = std::max(this->minimum_, std::min(this->getBottom(), value)); - - if (std::abs(this->currentValue_ - value) <= 0.0001) + value = std::clamp(value, this->minimum_, this->getBottom()); + if (areClose(this->currentValue_, value)) { + // value has not changed return; } @@ -159,7 +164,7 @@ void Scrollbar::setDesiredValue(qreal value, bool animated) this->desiredValueChanged_.invoke(); - this->atBottom_ = (this->getBottom() - value) <= 0.0001; + this->atBottom_ = areClose(this->getBottom(), value); if (animated && getSettings()->enableSmoothScrolling) { @@ -178,7 +183,7 @@ void Scrollbar::setDesiredValue(qreal value, bool animated) else { this->setCurrentValue(value); - this->resetMaximum(); + this->resetBounds(); } } } @@ -193,19 +198,14 @@ qreal Scrollbar::getMinimum() const return this->minimum_; } -qreal Scrollbar::getLargeChange() const +qreal Scrollbar::getPageSize() const { - return this->largeChange_; + return this->pageSize_; } qreal Scrollbar::getBottom() const { - return this->maximum_ - this->largeChange_; -} - -qreal Scrollbar::getSmallChange() const -{ - return this->smallChange_; + return this->maximum_ - this->pageSize_; } qreal Scrollbar::getDesiredValue() const @@ -222,8 +222,8 @@ qreal Scrollbar::getRelativeCurrentValue() const { // currentValue - minimum can be negative if minimum is incremented while // scrolling up to or down from the top when smooth scrolling is enabled. - return clamp(this->currentValue_ - this->minimum_, qreal(0.0), - this->currentValue_); + return std::clamp(this->currentValue_ - this->minimum_, 0.0, + this->currentValue_); } void Scrollbar::offset(qreal value) @@ -244,9 +244,9 @@ pajlada::Signals::NoArgSignal &Scrollbar::getDesiredValueChanged() void Scrollbar::setCurrentValue(qreal value) { value = std::max(this->minimum_, std::min(this->getBottom(), value)); - - if (std::abs(this->currentValue_ - value) <= 0.0001) + if (areClose(this->currentValue_, value)) { + // value has not changed return; } @@ -258,21 +258,24 @@ void Scrollbar::setCurrentValue(qreal value) void Scrollbar::printCurrentState(const QString &prefix) const { - qCDebug(chatterinoWidget) - << prefix // - << "Current value: " << this->getCurrentValue() // - << ". Maximum: " << this->getMaximum() // - << ". Minimum: " << this->getMinimum() // - << ". Large change: " << this->getLargeChange(); // + qCDebug(chatterinoWidget).nospace().noquote() + << prefix // + << " { currentValue: " << this->getCurrentValue() // + << ", desiredValue: " << this->getDesiredValue() // + << ", maximum: " << this->getMaximum() // + << ", minimum: " << this->getMinimum() // + << ", pageSize: " << this->getPageSize() // + << " }"; } -void Scrollbar::paintEvent(QPaintEvent *) +void Scrollbar::paintEvent(QPaintEvent * /*event*/) { - bool mouseOver = this->mouseOverIndex_ != -1; - int xOffset = mouseOver ? 0 : width() - int(4 * this->scale()); + bool mouseOver = this->mouseOverLocation_ != MouseLocation::Outside; + int xOffset = + mouseOver ? 0 : this->width() - static_cast(4.0F * this->scale()); QPainter painter(this); - painter.fillRect(rect(), this->theme->scrollbars.background); + painter.fillRect(this->rect(), this->theme->scrollbars.background); bool enableRedeemedHighlights = getSettings()->enableRedeemedHighlight; bool enableFirstMessageHighlights = @@ -280,16 +283,10 @@ void Scrollbar::paintEvent(QPaintEvent *) bool enableElevatedMessageHighlights = getSettings()->enableElevatedMessageHighlight; - // painter.fillRect(QRect(xOffset, 0, width(), this->buttonHeight), - // this->themeManager->ScrollbarArrow); - // painter.fillRect(QRect(xOffset, height() - this->buttonHeight, - // width(), this->buttonHeight), - // this->themeManager->ScrollbarArrow); - this->thumbRect_.setX(xOffset); // mouse over thumb - if (this->mouseDownIndex_ == 2) + if (this->mouseDownLocation_ == MouseLocation::InsideThumb) { painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumbSelected); @@ -301,23 +298,21 @@ void Scrollbar::paintEvent(QPaintEvent *) } // draw highlights - auto &snapshot = this->getHighlightSnapshot(); - size_t snapshotLength = snapshot.size(); - - if (snapshotLength == 0) + if (this->highlights_.empty()) { return; } + size_t nHighlights = this->highlights_.size(); int w = this->width(); - float y = 0; - float dY = float(this->height()) / float(snapshotLength); + float dY = + static_cast(this->height()) / static_cast(nHighlights); int highlightHeight = - int(std::ceil(std::max(this->scale() * 2, dY))); + static_cast(std::ceil(std::max(this->scale() * 2.0F, dY))); - for (size_t i = 0; i < snapshotLength; i++, y += dY) + for (size_t i = 0; i < nHighlights; i++) { - ScrollbarHighlight const &highlight = snapshot[i]; + const auto &highlight = this->highlights_[i]; if (highlight.isNull()) { @@ -344,16 +339,16 @@ void Scrollbar::paintEvent(QPaintEvent *) QColor color = highlight.getColor(); color.setAlpha(255); + int y = static_cast(dY * static_cast(i)); switch (highlight.getStyle()) { case ScrollbarHighlight::Default: { - painter.fillRect(w / 8 * 3, int(y), w / 4, highlightHeight, - color); + painter.fillRect(w / 8 * 3, y, w / 4, highlightHeight, color); } break; case ScrollbarHighlight::Line: { - painter.fillRect(0, int(y), w, 1, color); + painter.fillRect(0, y, w, 1, color); } break; @@ -362,52 +357,30 @@ void Scrollbar::paintEvent(QPaintEvent *) } } -void Scrollbar::resizeEvent(QResizeEvent *) +void Scrollbar::resizeEvent(QResizeEvent * /*event*/) { - this->resize(int(16 * this->scale()), this->height()); + this->resize(static_cast(16 * this->scale()), this->height()); } void Scrollbar::mouseMoveEvent(QMouseEvent *event) { - if (this->mouseDownIndex_ == -1) + if (this->mouseDownLocation_ == MouseLocation::Outside) { - int y = event->pos().y(); - - auto oldIndex = this->mouseOverIndex_; - - if (y < this->buttonHeight_) - { - this->mouseOverIndex_ = 0; - } - else if (y < this->thumbRect_.y()) - { - this->mouseOverIndex_ = 1; - } - else if (this->thumbRect_.contains(2, y)) - { - this->mouseOverIndex_ = 2; - } - else if (y < height() - this->buttonHeight_) - { - this->mouseOverIndex_ = 3; - } - else - { - this->mouseOverIndex_ = 4; - } - - if (oldIndex != this->mouseOverIndex_) + auto moveLocation = this->locationOfMouseEvent(event); + if (this->mouseOverLocation_ != moveLocation) { + this->mouseOverLocation_ = moveLocation; this->update(); } } - else if (this->mouseDownIndex_ == 2) + else if (this->mouseDownLocation_ == MouseLocation::InsideThumb) { - int delta = event->pos().y() - this->lastMousePosition_.y(); + qreal delta = + static_cast(event->pos().y() - this->lastMousePosition_.y()); this->setDesiredValue( this->desiredValue_ + - (qreal(delta) / std::max(0.00000002, this->trackHeight_)) * + (delta / std::max(0.00000002, this->trackHeight_)) * this->maximum_); } @@ -416,98 +389,80 @@ void Scrollbar::mouseMoveEvent(QMouseEvent *event) void Scrollbar::mousePressEvent(QMouseEvent *event) { - int y = event->pos().y(); - - if (y < this->buttonHeight_) - { - this->mouseDownIndex_ = 0; - } - else if (y < this->thumbRect_.y()) - { - this->mouseDownIndex_ = 1; - } - else if (this->thumbRect_.contains(2, y)) - { - this->mouseDownIndex_ = 2; - } - else if (y < height() - this->buttonHeight_) - { - this->mouseDownIndex_ = 3; - } - else - { - this->mouseDownIndex_ = 4; - } + this->mouseDownLocation_ = this->locationOfMouseEvent(event); + this->update(); } void Scrollbar::mouseReleaseEvent(QMouseEvent *event) { - int y = event->pos().y(); - - if (y < this->buttonHeight_) + auto releaseLocation = this->locationOfMouseEvent(event); + if (this->mouseDownLocation_ != releaseLocation) { - if (this->mouseDownIndex_ == 0) - { - this->setDesiredValue(this->desiredValue_ - this->smallChange_, - true); - } - } - else if (y < this->thumbRect_.y()) - { - if (this->mouseDownIndex_ == 1) - { - this->setDesiredValue(this->desiredValue_ - this->smallChange_, - true); - } - } - else if (this->thumbRect_.contains(2, y)) - { - // do nothing - } - else if (y < height() - this->buttonHeight_) - { - if (this->mouseDownIndex_ == 3) - { - this->setDesiredValue(this->desiredValue_ + this->smallChange_, - true); - } - } - else - { - if (this->mouseDownIndex_ == 4) - { - this->setDesiredValue(this->desiredValue_ + this->smallChange_, - true); - } + // Ignore event. User released the mouse from a different spot than + // they first clicked. For example, they clicked above the thumb, + // changed their mind, dragged the mouse below the thumb, and released. + this->mouseDownLocation_ = MouseLocation::Outside; + return; } - this->mouseDownIndex_ = -1; + switch (releaseLocation) + { + case MouseLocation::AboveThumb: + // Move scrollbar up a small bit. + this->setDesiredValue(this->desiredValue_ - SCROLL_DELTA, true); + break; + case MouseLocation::BelowThumb: + // Move scrollbar down a small bit. + this->setDesiredValue(this->desiredValue_ + SCROLL_DELTA, true); + break; + default: + break; + } + this->mouseDownLocation_ = MouseLocation::Outside; this->update(); } -void Scrollbar::leaveEvent(QEvent *) +void Scrollbar::leaveEvent(QEvent * /*event*/) { - this->mouseOverIndex_ = -1; - + this->mouseOverLocation_ = MouseLocation::Outside; this->update(); } void Scrollbar::updateScroll() { - this->trackHeight_ = this->height() - this->buttonHeight_ - - this->buttonHeight_ - MIN_THUMB_HEIGHT - 1; + this->trackHeight_ = this->height() - MIN_THUMB_HEIGHT - 1; auto div = std::max(0.0000001, this->maximum_ - this->minimum_); - this->thumbRect_ = QRect( - 0, - int((this->getRelativeCurrentValue()) / div * this->trackHeight_) + 1 + - this->buttonHeight_, - this->width(), - int(this->largeChange_ / div * this->trackHeight_) + MIN_THUMB_HEIGHT); + this->thumbRect_ = + QRect(0, + static_cast((this->getRelativeCurrentValue()) / div * + this->trackHeight_) + + 1, + this->width(), + static_cast(this->pageSize_ / div * this->trackHeight_) + + MIN_THUMB_HEIGHT); this->update(); } +Scrollbar::MouseLocation Scrollbar::locationOfMouseEvent( + QMouseEvent *event) const +{ + int y = event->pos().y(); + + if (y < this->thumbRect_.y()) + { + return MouseLocation::AboveThumb; + } + + if (this->thumbRect_.contains(2, y)) + { + return MouseLocation::InsideThumb; + } + + return MouseLocation::BelowThumb; +} + } // namespace chatterino diff --git a/src/widgets/Scrollbar.hpp b/src/widgets/Scrollbar.hpp index d025539c1..08a843586 100644 --- a/src/widgets/Scrollbar.hpp +++ b/src/widgets/Scrollbar.hpp @@ -1,11 +1,10 @@ #pragma once -#include "messages/LimitedQueue.hpp" #include "widgets/BaseWidget.hpp" #include "widgets/helper/ScrollbarHighlight.hpp" +#include #include -#include #include #include @@ -13,41 +12,119 @@ namespace chatterino { class ChannelView; +/// @brief A scrollbar for views with partially laid out items +/// +/// This scrollbar is made for views that only lay out visible items. This is +/// the case for a @a ChannelView for example. There, only the visible messages +/// are laid out. For a traditional scrollbar, all messages would need to be +/// laid out to be able to compute the total height of all items. However, for +/// these messages this isn't possible. +/// +/// To avoid having to lay out all items, this scrollbar tracks the position of +/// the content in messages (as opposed to pixels). The position is given by +/// `currentValue` which refers to the index of the message at the top plus a +/// fraction inside the message. The position can be animated to have a smooth +/// scrolling effect. In this case, `currentValue` refers to the displayed +/// position and `desiredValue` refers to the position the scrollbar is set to +/// be at after the animation. The latter is used for example to check if the +/// scrollbar is at the bottom. +/// +/// `minimum` and `maximum` are used to map scrollbar positions to +/// (message-)buffer indices. The buffer is of size `maximum - minimum` and an +/// index is computed by `scrollbarPos - minimum` - thus a scrollbar position +/// of a message is at `index + minimum. +/// +/// @cond src-only +/// +/// The following illustrates a scrollbar in a channel view with seven +/// messages. The scrollbar is at the bottom. No animation is active, thus +/// `currentValue = desiredValue`. +/// +/// ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐←╌╌╌ minimum +/// Alice: This message is quite = 0 +/// ┬ ╭─────────────────────────────────╮←╮ +/// │ │ long, so it gets wrapped │ ┆ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ ╰╌╌╌ currentValue +/// │ │ Bob: are you sure? │ = 0.5 +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ = desiredValue +/// pageSize ╌╌╌┤ │ Alice: Works for me... try for │ = maximum +/// = 6.5 │ │ yourself │ - pageSize +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ = bottom +/// │ │ Bob: I'm trying to get my really│ ⇒ atBottom = true +/// │ │ long message to wrap so I can │ +/// │ │ debug this issue I'm facing... │ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +/// │ │ Bob: Omg it worked │ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +/// │ │ Alice: That's amazing! ╭┤ ┬ +/// │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌││ ├╌╌ thumbRect.height() +/// │ │ Bob: you're right ╰┤ ┴ +/// ┴╭→╰─────────────────────────────────╯ +/// ┆ +/// maximum +/// = 7 +/// @endcond +/// +/// When messages are added at the bottom, both maximum and minimum are offset +/// by 1 and after a layout, the desired value is updated, causing the content +/// to move. Afterwards, the bounds are reset (potentially waiting for the +/// animation to finish). +/// +/// While scrolling is paused, the desired (and current) value won't be +/// updated. However, messages can still come in and "shift" the values in the +/// backing ring-buffer. If the current value would be used, the messages would +/// still shift upwards (just at a different offset). To avoid this, there's a +/// _relative current value_, which is `currentValue - minimum`. It's the +/// actual index of the top message in the buffer. Since the minimum is shifted +/// by 1 when messages come in, the view will remain idle (visually). class Scrollbar : public BaseWidget { Q_OBJECT public: - Scrollbar(size_t messagesLimit, ChannelView *parent = nullptr); + Scrollbar(size_t messagesLimit, ChannelView *parent); + /// Return a copy of the highlights + /// + /// Should only be used for tests + boost::circular_buffer getHighlights() const; void addHighlight(ScrollbarHighlight highlight); void addHighlightsAtStart( const std::vector &highlights_); void replaceHighlight(size_t index, ScrollbarHighlight replacement); - void pauseHighlights(); - void unpauseHighlights(); void clearHighlights(); void scrollToBottom(bool animate = false); void scrollToTop(bool animate = false); bool isAtBottom() const; + qreal getMaximum() const; void setMaximum(qreal value); void offsetMaximum(qreal value); - void resetMaximum(); + + qreal getMinimum() const; void setMinimum(qreal value); void offsetMinimum(qreal value); - void setLargeChange(qreal value); - void setSmallChange(qreal value); - void setDesiredValue(qreal value, bool animated = false); - qreal getMaximum() const; - qreal getMinimum() const; - qreal getLargeChange() const; - qreal getBottom() const; - qreal getSmallChange() const; + + void resetBounds(); + + qreal getPageSize() const; + void setPageSize(qreal value); + qreal getDesiredValue() const; + void setDesiredValue(qreal value, bool animated = false); + + /// The bottom-most scroll position + qreal getBottom() const; qreal getCurrentValue() const; + + /// @brief The current value relative to the minimum + /// + /// > currentValue - minimum + /// + /// This should be used as an index into a buffer of messages, as it is + /// unaffected by simultaneous shifts of minimum and maximum. qreal getRelativeCurrentValue() const; // offset the desired value without breaking smooth scolling @@ -56,47 +133,54 @@ public: pajlada::Signals::NoArgSignal &getDesiredValueChanged(); void setCurrentValue(qreal value); - void printCurrentState(const QString &prefix = QString()) const; + void printCurrentState( + const QString &prefix = QStringLiteral("Scrollbar")) const; Q_PROPERTY(qreal desiredValue_ READ getDesiredValue WRITE setDesiredValue) protected: - void paintEvent(QPaintEvent *) override; - void resizeEvent(QResizeEvent *) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - void leaveEvent(QEvent *) override; + void leaveEvent(QEvent *event) override; private: Q_PROPERTY(qreal currentValue_ READ getCurrentValue WRITE setCurrentValue) - LimitedQueueSnapshot &getHighlightSnapshot(); void updateScroll(); - QMutex mutex_; + enum class MouseLocation { + /// The mouse is positioned outside the scrollbar + Outside, + /// The mouse is positioned inside the scrollbar, but above the thumb (the thing you can drag inside the scrollbar) + AboveThumb, + /// The mouse is positioned inside the scrollbar, and on top of the thumb + InsideThumb, + /// The mouse is positioned inside the scrollbar, but below the thumb + BelowThumb, + }; + + MouseLocation locationOfMouseEvent(QMouseEvent *event) const; QPropertyAnimation currentValueAnimation_; - LimitedQueue highlights_; - bool highlightsPaused_{false}; - LimitedQueueSnapshot highlightSnapshot_; + boost::circular_buffer highlights_; bool atBottom_{false}; - int mouseOverIndex_ = -1; - int mouseDownIndex_ = -1; + MouseLocation mouseOverLocation_ = MouseLocation::Outside; + MouseLocation mouseDownLocation_ = MouseLocation::Outside; QPoint lastMousePosition_; - int buttonHeight_ = 0; int trackHeight_ = 100; QRect thumbRect_; qreal maximum_ = 0; qreal minimum_ = 0; - qreal largeChange_ = 0; - qreal smallChange_ = 5; + qreal pageSize_ = 0; qreal desiredValue_ = 0; qreal currentValue_ = 0; diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 71a18a0a3..fd80d7e95 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -350,11 +350,11 @@ void EmotePopup::addShortcuts() auto &scrollbar = channelView->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index 32ee5a529..4d3dd3a83 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -51,11 +51,11 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, Split *split) auto &scrollbar = this->ui_.threadView->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 953be229c..fff4fefc1 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -164,11 +164,11 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split) auto &scrollbar = this->ui_.latestMessages->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c5a679727..819b7d0c0 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -744,7 +744,7 @@ void ChannelView::updateScrollbar( if (h < 0) // break condition { - this->scrollBar_->setLargeChange( + this->scrollBar_->setPageSize( (messages.size() - i) + qreal(h) / std::max(1, message->getHeight())); @@ -778,7 +778,7 @@ void ChannelView::clearMessages() // Clear all stored messages in this chat widget this->messages_.clear(); this->scrollBar_->clearHighlights(); - this->scrollBar_->resetMaximum(); + this->scrollBar_->resetBounds(); this->scrollBar_->setMaximum(0); this->scrollBar_->setMinimum(0); this->queueLayout(); @@ -1277,7 +1277,7 @@ void ChannelView::messagesUpdated() this->messages_.clear(); this->scrollBar_->clearHighlights(); - this->scrollBar_->resetMaximum(); + this->scrollBar_->resetBounds(); this->scrollBar_->setMaximum(qreal(snapshot.size())); this->scrollBar_->setMinimum(0); this->lastMessageHasAlternateBackground_ = false; diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 81b0e2f65..1e34aefbf 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -553,11 +553,11 @@ void Split::addShortcuts() auto &scrollbar = this->getChannelView().getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ea086b13..8288664df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -43,6 +43,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp # Add your new file above this line! ) diff --git a/tests/src/ModerationAction.cpp b/tests/src/ModerationAction.cpp index ce32fb39d..75daf8e3e 100644 --- a/tests/src/ModerationAction.cpp +++ b/tests/src/ModerationAction.cpp @@ -5,8 +5,8 @@ #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/Scrollbar.cpp b/tests/src/Scrollbar.cpp new file mode 100644 index 000000000..98ca9a640 --- /dev/null +++ b/tests/src/Scrollbar.cpp @@ -0,0 +1,187 @@ +#include "widgets/Scrollbar.hpp" + +#include "Application.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" +#include "Test.hpp" +#include "widgets/helper/ScrollbarHighlight.hpp" + +#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; +}; + +} // namespace + +TEST(Scrollbar, AddHighlight) +{ + MockApplication mockApplication; + + Scrollbar scrollbar(10, nullptr); + EXPECT_EQ(scrollbar.getHighlights().size(), 0); + + for (int i = 0; i < 15; ++i) + { + auto color = std::make_shared(i, 0, 0); + ScrollbarHighlight scrollbarHighlight{color}; + scrollbar.addHighlight(scrollbarHighlight); + } + + EXPECT_EQ(scrollbar.getHighlights().size(), 10); + auto highlights = scrollbar.getHighlights(); + for (int i = 0; i < 10; ++i) + { + auto highlight = highlights[i]; + EXPECT_EQ(highlight.getColor().red(), i + 5); + } +} + +TEST(Scrollbar, AddHighlightsAtStart) +{ + MockApplication mockApplication; + + Scrollbar scrollbar(10, nullptr); + EXPECT_EQ(scrollbar.getHighlights().size(), 0); + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(1, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 1); + EXPECT_EQ(highlights[0].getColor().red(), 1); + } + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(2, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 2); + EXPECT_EQ(highlights[0].getColor().red(), 2); + EXPECT_EQ(highlights[1].getColor().red(), 1); + } + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(4, 0, 0), + }, + { + std::make_shared(3, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 4); + EXPECT_EQ(highlights[0].getColor().red(), 4); + EXPECT_EQ(highlights[1].getColor().red(), 3); + EXPECT_EQ(highlights[2].getColor().red(), 2); + EXPECT_EQ(highlights[3].getColor().red(), 1); + } + + { + // Adds as many as it can, in reverse order + scrollbar.addHighlightsAtStart({ + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(10, 0, 0)}, + {std::make_shared(9, 0, 0)}, + {std::make_shared(8, 0, 0)}, + {std::make_shared(7, 0, 0)}, + {std::make_shared(6, 0, 0)}, + {std::make_shared(5, 0, 0)}, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 10); + for (const auto &highlight : highlights) + { + std::cout << highlight.getColor().red() << '\n'; + } + EXPECT_EQ(highlights[0].getColor().red(), 10); + EXPECT_EQ(highlights[1].getColor().red(), 9); + EXPECT_EQ(highlights[2].getColor().red(), 8); + EXPECT_EQ(highlights[3].getColor().red(), 7); + EXPECT_EQ(highlights[4].getColor().red(), 6); + EXPECT_EQ(highlights[5].getColor().red(), 5); + EXPECT_EQ(highlights[6].getColor().red(), 4); + EXPECT_EQ(highlights[7].getColor().red(), 3); + EXPECT_EQ(highlights[8].getColor().red(), 2); + EXPECT_EQ(highlights[9].getColor().red(), 1); + } + + { + // Adds as many as it can, in reverse order + // Since the highlights are already full, nothing will be added + scrollbar.addHighlightsAtStart({ + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 10); + for (const auto &highlight : highlights) + { + std::cout << highlight.getColor().red() << '\n'; + } + EXPECT_EQ(highlights[0].getColor().red(), 10); + EXPECT_EQ(highlights[1].getColor().red(), 9); + EXPECT_EQ(highlights[2].getColor().red(), 8); + EXPECT_EQ(highlights[3].getColor().red(), 7); + EXPECT_EQ(highlights[4].getColor().red(), 6); + EXPECT_EQ(highlights[5].getColor().red(), 5); + EXPECT_EQ(highlights[6].getColor().red(), 4); + EXPECT_EQ(highlights[7].getColor().red(), 3); + EXPECT_EQ(highlights[8].getColor().red(), 2); + EXPECT_EQ(highlights[9].getColor().red(), 1); + } +} From febcf464fe9f9caa78fbfb0d3fb2db8cb372ee22 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 13:59:14 +0200 Subject: [PATCH 68/71] Use Qt's High-DPI scaling on Windows (#4868) --- CHANGELOG.md | 2 + resources/qss/settings.qss | 6 +- src/RunGui.cpp | 4 - src/main.cpp | 5 - src/messages/MessageElement.cpp | 10 +- src/messages/layouts/MessageLayout.cpp | 13 +- src/messages/layouts/MessageLayout.hpp | 5 +- .../layouts/MessageLayoutContainer.cpp | 8 +- .../layouts/MessageLayoutContainer.hpp | 12 +- src/widgets/AttachedWindow.cpp | 8 +- src/widgets/BaseWidget.cpp | 13 - src/widgets/BaseWidget.hpp | 2 - src/widgets/BaseWindow.cpp | 537 +++++++++++------- src/widgets/BaseWindow.hpp | 9 +- src/widgets/Label.cpp | 19 +- src/widgets/TooltipEntryWidget.cpp | 1 + src/widgets/dialogs/SettingsDialog.cpp | 21 +- src/widgets/helper/Button.cpp | 54 +- src/widgets/helper/ChannelView.cpp | 29 +- src/widgets/helper/NotebookTab.cpp | 19 +- tests/src/MessageLayout.cpp | 2 +- 21 files changed, 459 insertions(+), 320 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 631b3eb1d..ef547e8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## Unversioned - Major: Release plugins alpha. (#5288) +- Major: Improve high-DPI support on Windows. (#4868) - Minor: Add option to customise Moderation buttons with images. (#5369) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334) diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index 6d5114423..93c69b603 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -1,11 +1,11 @@ * { - font-size: px; + font-size: 14px; font-family: "Segoe UI"; } QCheckBox::indicator { - width: px; - height: px; + width: 14px; + height: 14px; } chatterino--ComboBox { diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 13012957d..6fba9c6af 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -86,10 +86,6 @@ namespace { QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); #endif -#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - QApplication::setStyle(QStyleFactory::create("Fusion")); #ifndef Q_OS_MAC diff --git a/src/main.cpp b/src/main.cpp index ef59af0c5..8da92a45c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,11 +26,6 @@ using namespace chatterino; int main(int argc, char **argv) { - // TODO: This is a temporary fix (see #4552). -#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); -#endif - QApplication a(argc, argv); QCoreApplication::setApplicationName("chatterino"); diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index b11e82be1..3d5ab64e7 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -155,8 +155,8 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + auto image = this->emote_->images.getImageOrLoaded( + container.getImageScale()); if (image->isEmpty()) { return; @@ -210,7 +210,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto images = this->getLoadedImages(container.getScale()); + auto images = this->getLoadedImages(container.getImageScale()); if (images.empty()) { return; @@ -364,7 +364,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + this->emote_->images.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; @@ -798,7 +798,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { const auto &image = - this->images_.getImageOrLoaded(container.getScale()); + this->images_.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 126bb40c9..8a20b05cc 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -74,7 +74,8 @@ int MessageLayout::getWidth() const // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, +bool MessageLayout::layout(int width, float scale, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer) { // BenchmarkGuard benchmark("MessageLayout::layout()"); @@ -106,6 +107,8 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, // check if dpi changed layoutRequired |= this->scale_ != scale; this->scale_ = scale; + layoutRequired |= this->imageScale_ != imageScale; + this->imageScale_ = imageScale; if (!layoutRequired) { @@ -148,7 +151,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) bool hideSimilar = getSettings()->hideSimilar; bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage); - this->container_.beginLayout(width, this->scale_, messageFlags); + this->container_.beginLayout(width, this->scale_, this->imageScale_, + messageFlags); for (const auto &element : this->message_->elements) { @@ -288,16 +292,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) } // Create new buffer -#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) this->buffer_ = std::make_unique( int(width * painter.device()->devicePixelRatioF()), int(this->container_.getHeight() * painter.device()->devicePixelRatioF())); this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF()); -#else - this->buffer_ = std::make_unique( - width, std::max(16, this->container_.getHeight())); -#endif this->bufferValid_ = false; DebugCount::increase("message drawing buffers"); diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index f54f57d2a..01958ddf2 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -56,8 +56,8 @@ public: MessageLayoutFlags flags; - bool layout(int width, float scale_, MessageElementFlags flags, - bool shouldInvalidateBuffer); + bool layout(int width, float scale_, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer); // Painting MessagePaintResult paint(const MessagePaintContext &ctx); @@ -128,6 +128,7 @@ private: int currentLayoutWidth_ = -1; int layoutState_ = -1; float scale_ = -1; + float imageScale_ = -1.F; MessageElementFlags currentWordFlags_; #ifdef FOURTF diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 29d70e0a1..15a7d71af 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -30,7 +30,7 @@ constexpr const QMargins MARGIN{8, 4, 8, 4}; namespace chatterino { void MessageLayoutContainer::beginLayout(int width, float scale, - MessageFlags flags) + float imageScale, MessageFlags flags) { this->elements_.clear(); this->lines_.clear(); @@ -45,6 +45,7 @@ void MessageLayoutContainer::beginLayout(int width, float scale, this->width_ = width; this->height_ = 0; this->scale_ = scale; + this->imageScale_ = imageScale; this->flags_ = flags; auto mediumFontMetrics = getIApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale); @@ -526,6 +527,11 @@ float MessageLayoutContainer::getScale() const return this->scale_; } +float MessageLayoutContainer::getImageScale() const +{ + return this->imageScale_; +} + bool MessageLayoutContainer::isCollapsed() const { return this->isCollapsed_; diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index ed3c1a7a6..dde3f4d45 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -32,7 +32,8 @@ struct MessageLayoutContainer { * This will reset all line calculations, and will be considered incomplete * until the accompanying end function has been called */ - void beginLayout(int width_, float scale_, MessageFlags flags_); + void beginLayout(int width, float scale, float imageScale, + MessageFlags flags); /** * Finish the layout process of this message @@ -146,6 +147,11 @@ struct MessageLayoutContainer { */ float getScale() const; + /** + * Returns the image scale + */ + float getImageScale() const; + /** * Returns true if this message is collapsed */ @@ -270,6 +276,10 @@ private: // variables float scale_ = 1.F; + /** + * Scale factor for images + */ + float imageScale_ = 1.F; int width_ = 0; MessageFlags flags_{}; /** diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 5ce232a1f..b83afb65d 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -270,20 +270,22 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) } float scale = 1.f; + float ourScale = 1.F; if (auto dpi = getWindowDpi(attached)) { scale = *dpi / 96.f; + ourScale = scale / this->devicePixelRatio(); for (auto w : this->ui_.split->findChildren()) { - w->setOverrideScale(scale); + w->setOverrideScale(ourScale); } - this->ui_.split->setOverrideScale(scale); + this->ui_.split->setOverrideScale(ourScale); } if (this->height_ != -1) { - this->ui_.split->setFixedWidth(int(this->width_ * scale)); + this->ui_.split->setFixedWidth(int(this->width_ * ourScale)); // offset int o = this->fullscreen_ ? 0 : 8; diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 5302d0397..5e2c932fd 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -120,19 +120,6 @@ void BaseWidget::setScaleIndependantHeight(int value) QSize(this->scaleIndependantSize_.width(), value)); } -float BaseWidget::qtFontScale() const -{ - if (auto *window = dynamic_cast(this->window())) - { - // ensure no div by 0 - return this->scale() / std::max(0.01f, window->nativeScale_); - } - else - { - return this->scale(); - } -} - void BaseWidget::childEvent(QChildEvent *event) { if (event->added()) diff --git a/src/widgets/BaseWidget.hpp b/src/widgets/BaseWidget.hpp index 2e9c04728..4fdc421cd 100644 --- a/src/widgets/BaseWidget.hpp +++ b/src/widgets/BaseWidget.hpp @@ -34,8 +34,6 @@ public: void setScaleIndependantWidth(int value); void setScaleIndependantHeight(int value); - float qtFontScale() const; - protected: void childEvent(QChildEvent *) override; void showEvent(QShowEvent *) override; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 81ed90b28..5a1df70af 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -29,12 +29,163 @@ # pragma comment(lib, "Dwmapi.lib") # include - -# define WM_DPICHANGED 0x02E0 +# include #endif #include "widgets/helper/TitlebarButton.hpp" +namespace { + +#ifdef USEWINSDK + +// From kHiddenTaskbarSize in Firefox +constexpr UINT HIDDEN_TASKBAR_SIZE = 2; + +bool isWindows11OrGreater() +{ + static const bool result = [] { + // This calls RtlGetVersion under the hood so we don't have to. + // The micro version corresponds to dwBuildNumber. + auto version = QOperatingSystemVersion::current(); + return (version.majorVersion() > 10) || + (version.microVersion() >= 22000); + }(); + + return result; +} + +/// Finds the taskbar HWND on a specific monitor (or any) +HWND findTaskbarWindow(LPRECT rcMon = nullptr) +{ + HWND taskbar = nullptr; + RECT taskbarRect; + // return value of IntersectRect, unused + RECT intersectionRect; + + while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd", + nullptr)) != nullptr) + { + if (!rcMon) + { + // no monitor was specified, return the first encountered window + break; + } + if (GetWindowRect(taskbar, &taskbarRect) != 0 && + IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0) + { + // taskbar intersects with the monitor - this is the one + break; + } + } + + return taskbar; +} + +/// Gets the edge of the taskbar if it's automatically hidden +std::optional hiddenTaskbarEdge(LPRECT rcMon = nullptr) +{ + HWND taskbar = findTaskbarWindow(rcMon); + if (!taskbar) + { + return std::nullopt; + } + + APPBARDATA state = {sizeof(state), taskbar}; + APPBARDATA pos = {sizeof(pos), taskbar}; + + auto appBarState = + static_cast(SHAppBarMessage(ABM_GETSTATE, &state)); + if ((appBarState & ABS_AUTOHIDE) == 0) + { + return std::nullopt; + } + + if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0) + { + qCDebug(chatterinoApp) << "Failed to get taskbar pos"; + return ABE_BOTTOM; + } + + return pos.uEdge; +} + +/// @brief Gets the window borders for @a hwnd +/// +/// Each side of the returned RECT has the correct sign, so they can be added +/// to a window rect. +/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}. +RECT windowBordersFor(HWND hwnd, bool isMaximized) +{ + RECT margins{0, 0, 0, 0}; + + auto addBorders = isMaximized || isWindows11OrGreater(); + if (addBorders) + { + auto dpi = GetDpiForWindow(hwnd); + auto systemMetric = [&](auto index) { + if (dpi != 0) + { + return GetSystemMetricsForDpi(index, dpi); + } + return GetSystemMetrics(index); + }; + + auto paddedBorder = systemMetric(SM_CXPADDEDBORDER); + auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder; + auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder; + + margins.left += borderWidth; + margins.right -= borderWidth; + if (isMaximized) + { + margins.top += borderHeight; + } + margins.bottom -= borderHeight; + } + + if (isMaximized) + { + auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(mi); + auto *monitor = [&]() -> LPRECT { + if (GetMonitorInfo(hMonitor, &mi)) + { + return &mi.rcMonitor; + } + return nullptr; + }(); + + auto edge = hiddenTaskbarEdge(monitor); + if (edge) + { + switch (*edge) + { + case ABE_LEFT: + margins.left += HIDDEN_TASKBAR_SIZE; + break; + case ABE_RIGHT: + margins.right -= HIDDEN_TASKBAR_SIZE; + break; + case ABE_TOP: + margins.top += HIDDEN_TASKBAR_SIZE; + break; + case ABE_BOTTOM: + margins.bottom -= HIDDEN_TASKBAR_SIZE; + break; + default: + break; + } + } + } + + return margins; +} + +#endif + +} // namespace + namespace chatterino { BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) @@ -117,95 +268,80 @@ float BaseWindow::scale() const return std::max(0.01f, this->overrideScale().value_or(this->scale_)); } -float BaseWindow::qtFontScale() const -{ - return this->scale() / std::max(0.01F, this->nativeScale_); -} - void BaseWindow::init() { #ifdef USEWINSDK if (this->hasCustomWindowFrame()) { // CUSTOM WINDOW FRAME - QVBoxLayout *layout = new QVBoxLayout(); + auto *layout = new QVBoxLayout(this); this->ui_.windowLayout = layout; - layout->setContentsMargins(1, 1, 1, 1); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - this->setLayout(layout); + + if (!this->frameless_) { - if (!this->frameless_) - { - QHBoxLayout *buttonLayout = this->ui_.titlebarBox = - new QHBoxLayout(); - buttonLayout->setContentsMargins(0, 0, 0, 0); - layout->addLayout(buttonLayout); + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); + buttonLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(buttonLayout); - // title - Label *title = new Label; - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { - title->setText(text); - }); + // title + Label *title = new Label; + QObject::connect(this, &QWidget::windowTitleChanged, + [title](const QString &text) { + title->setText(text); + }); - QSizePolicy policy(QSizePolicy::Ignored, - QSizePolicy::Preferred); - policy.setHorizontalStretch(1); - title->setSizePolicy(policy); - buttonLayout->addWidget(title); - this->ui_.titleLabel = title; + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui_.titleLabel = title; - // buttons - TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); - TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); - TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButtonStyle::Close); + // buttons + auto *minButton = new TitleBarButton; + minButton->setButtonStyle(TitleBarButtonStyle::Minimize); + auto *maxButton = new TitleBarButton; + maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); + auto *exitButton = new TitleBarButton; + exitButton->setButtonStyle(TitleBarButtonStyle::Close); - QObject::connect(_minButton, &TitleBarButton::leftClicked, this, - [this] { - this->setWindowState(Qt::WindowMinimized | - this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::leftClicked, this, - [this, _maxButton] { - this->setWindowState( - _maxButton->getButtonStyle() != + QObject::connect(minButton, &TitleBarButton::leftClicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect( + maxButton, &TitleBarButton::leftClicked, this, + [this, maxButton] { + this->setWindowState(maxButton->getButtonStyle() != TitleBarButtonStyle::Maximize ? Qt::WindowActive : Qt::WindowMaximized); - }); - QObject::connect(_exitButton, &TitleBarButton::leftClicked, - this, [this] { - this->close(); - }); + }); + QObject::connect(exitButton, &TitleBarButton::leftClicked, this, + [this] { + this->close(); + }); - this->ui_.titlebarButtons = new TitleBarButtons( - this, _minButton, _maxButton, _exitButton); + this->ui_.titlebarButtons = + new TitleBarButtons(this, minButton, maxButton, exitButton); - this->ui_.buttons.push_back(_minButton); - this->ui_.buttons.push_back(_maxButton); - this->ui_.buttons.push_back(_exitButton); + this->ui_.buttons.push_back(minButton); + this->ui_.buttons.push_back(maxButton); + this->ui_.buttons.push_back(exitButton); - // buttonLayout->addStretch(1); - buttonLayout->addWidget(_minButton); - buttonLayout->addWidget(_maxButton); - buttonLayout->addWidget(_exitButton); - buttonLayout->setSpacing(0); - } + buttonLayout->addWidget(minButton); + buttonLayout->addWidget(maxButton); + buttonLayout->addWidget(exitButton); + buttonLayout->setSpacing(0); } + this->ui_.layoutBase = new BaseWidget(this); this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1); layout->addWidget(this->ui_.layoutBase); } - -// DPI -// auto dpi = getWindowDpi(this->safeHWND()); - -// if (dpi) { -// this->scale = dpi.value() / 96.f; -// } #endif // TopMost flag overrides setting @@ -571,29 +707,8 @@ void BaseWindow::resizeEvent(QResizeEvent *) } #ifdef USEWINSDK - if (this->hasCustomWindowFrame() && !this->isResizeFixing_) - { - this->isResizeFixing_ = true; - QTimer::singleShot(50, this, [this] { - auto hwnd = this->safeHWND(); - if (!hwnd) - { - this->isResizeFixing_ = false; - return; - } - RECT rect; - ::GetWindowRect(*hwnd, &rect); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - QTimer::singleShot(10, this, [this] { - this->isResizeFixing_ = false; - }); - }); - } - this->calcButtonsSizes(); + this->updateRealSize(); #endif } @@ -655,10 +770,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, switch (msg->message) { - case WM_DPICHANGED: - returnValue = this->handleDPICHANGED(msg); - break; - case WM_SHOWWINDOW: returnValue = this->handleSHOWWINDOW(msg); break; @@ -697,12 +808,15 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, { *result = 0; returnValue = true; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + + // TODO(nerix): use TrackMouseEvent here this->ui_.titlebarButtons->hover(msg->wParam, globalPos); this->lastEventWasNcMouseMove_ = true; } @@ -748,12 +862,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, *result = 0; auto ht = msg->wParam; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + if (msg->message == WM_NCLBUTTONDOWN) { this->ui_.titlebarButtons->mousePress(ht, globalPos); @@ -784,7 +900,7 @@ void BaseWindow::scaleChangedEvent(float scale) #endif this->setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); + getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale())); } void BaseWindow::paintEvent(QPaintEvent *) @@ -802,10 +918,9 @@ void BaseWindow::paintEvent(QPaintEvent *) void BaseWindow::updateScale() { - auto scale = - this->nativeScale_ * (this->flags_.has(DisableCustomScaling) - ? 1 - : getSettings()->getClampedUiScale()); + auto scale = this->flags_.has(DisableCustomScaling) + ? 1 + : getSettings()->getClampedUiScale(); this->setScale(scale); @@ -815,6 +930,22 @@ void BaseWindow::updateScale() } } +#ifdef USEWINSDK +void BaseWindow::updateRealSize() +{ + auto hwnd = this->safeHWND(); + if (!hwnd) + { + return; + } + + RECT real; + ::GetWindowRect(*hwnd, &real); + this->realBounds_ = QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); +} +#endif + void BaseWindow::calcButtonsSizes() { if (!this->shown_) @@ -846,34 +977,28 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter) { QColor bg = this->overrideBackgroundColor_.value_or( this->theme->window.background); - painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3), - bg); + if (this->isMaximized_) + { + painter.fillRect(this->rect(), bg); + } + else + { + // Draw a border that's exactly 1px wide + // + // There is a bug where the border can get px wide while dragging. + // this "fixes" itself when deselecting the window. + auto dpr = this->devicePixelRatio(); + if (dpr != 1) + { + painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr)); + } + painter.fillRect(1, 1, this->realBounds_.width() - 2, + this->realBounds_.height() - 2, bg); + } } #endif } -bool BaseWindow::handleDPICHANGED(MSG *msg) -{ -#ifdef USEWINSDK - int dpi = HIWORD(msg->wParam); - - float _scale = dpi / 96.f; - - auto *prcNewWindow = reinterpret_cast(msg->lParam); - SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, - prcNewWindow->right - prcNewWindow->left, - prcNewWindow->bottom - prcNewWindow->top, - SWP_NOZORDER | SWP_NOACTIVATE); - - this->nativeScale_ = _scale; - this->updateScale(); - - return true; -#else - return false; -#endif -} - bool BaseWindow::handleSHOWWINDOW(MSG *msg) { #ifdef USEWINSDK @@ -883,16 +1008,6 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) return true; } - if (auto dpi = getWindowDpi(msg->hwnd)) - { - float currentScale = (float)dpi.value() / 96.F; - if (currentScale != this->nativeScale_) - { - this->nativeScale_ = currentScale; - this->updateScale(); - } - } - if (!this->shown_) { this->shown_ = true; @@ -906,14 +1021,12 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) if (!this->initalBounds_.isNull()) { - ::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(), - this->initalBounds_.y(), this->initalBounds_.width(), - this->initalBounds_.height(), - SWP_NOZORDER | SWP_NOACTIVATE); + this->setGeometry(this->initalBounds_); this->currentBounds_ = this->initalBounds_; } this->calcButtonsSizes(); + this->updateRealSize(); } return true; @@ -929,23 +1042,54 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) #endif { #ifdef USEWINSDK - if (this->hasCustomWindowFrame()) + if (!this->hasCustomWindowFrame()) { - if (msg->wParam == TRUE) - { - // remove 1 extra pixel on top of custom frame - auto *ncp = reinterpret_cast(msg->lParam); - if (ncp) - { - ncp->lppos->flags |= SWP_NOREDRAW; - ncp->rgrc[0].top -= 1; - } - } + return false; + } + if (msg->wParam != TRUE) + { *result = 0; return true; } - return false; + + auto *params = reinterpret_cast(msg->lParam); + auto *r = ¶ms->rgrc[0]; + + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 && + (wp.showCmd == SW_SHOWMAXIMIZED); + + auto borders = windowBordersFor(msg->hwnd, this->isMaximized_); + r->left += borders.left; + r->top += borders.top; + r->right += borders.right; + r->bottom += borders.bottom; + + if (borders.left != 0 || borders.top != 0 || borders.right != 0 || + borders.bottom != 0) + { + // We added borders -> we changed the rect, so we can't return + // WVR_VALIDRECTS + *result = 0; + return true; + } + + // This is an attempt at telling Windows to not redraw (or at least to do a + // better job at redrawing) the window. There is a long list of tricks + // people tried to prevent this at + // https://stackoverflow.com/q/53000291/16300717 + // + // We set the source and destination rectangles to a 1x1 rectangle at the + // top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve + // some parts of the window image. + QPoint fixed = {r->left, r->top}; + params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + *result = WVR_VALIDRECTS; + + return true; #else return false; #endif @@ -962,28 +1106,11 @@ bool BaseWindow::handleSIZE(MSG *msg) } else if (this->hasCustomWindowFrame()) { - if (msg->wParam == SIZE_MAXIMIZED) - { - auto offset = - int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96); - - this->ui_.windowLayout->setContentsMargins(offset, offset, - offset, offset); - } - else - { - this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); - } - this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED; if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->currentBounds_ = - QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); + this->currentBounds_ = this->geometry(); } this->useNextBounds_.stop(); @@ -993,6 +1120,12 @@ bool BaseWindow::handleSIZE(MSG *msg) // the minimize button, so we have to emulate it. this->ui_.titlebarButtons->leave(); } + + RECT real; + ::GetWindowRect(msg->hwnd, &real); + this->realBounds_ = + QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); } } return false; @@ -1006,11 +1139,7 @@ bool BaseWindow::handleMOVE(MSG *msg) #ifdef USEWINSDK if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->nextBounds_ = QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); - + this->nextBounds_ = this->geometry(); this->useNextBounds_.start(10); } #endif @@ -1024,31 +1153,37 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) #endif { #ifdef USEWINSDK - const LONG border_width = 8; // in pixels - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); + const LONG borderWidth = 8; // in device independent pixels - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); + auto rect = this->rect(); - QPoint point(x - winrect.left, y - winrect.top); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint point(p.x, p.y); + point /= this->devicePixelRatio(); + + auto x = point.x(); + auto y = point.y(); if (this->hasCustomWindowFrame()) { *result = 0; - bool resizeWidth = minimumWidth() != maximumWidth(); - bool resizeHeight = minimumHeight() != maximumHeight(); + bool resizeWidth = + minimumWidth() != maximumWidth() && !this->isMaximized(); + bool resizeHeight = + minimumHeight() != maximumHeight() && !this->isMaximized(); if (resizeWidth) { // left border - if (x < winrect.left + border_width) + if (x < rect.left() + borderWidth) { *result = HTLEFT; } // right border - if (x >= winrect.right - border_width) + if (x >= rect.right() - borderWidth) { *result = HTRIGHT; } @@ -1056,12 +1191,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeHeight) { // bottom border - if (y >= winrect.bottom - border_width) + if (y >= rect.bottom() - borderWidth) { *result = HTBOTTOM; } // top border - if (y < winrect.top + border_width) + if (y < rect.top() + borderWidth) { *result = HTTOP; } @@ -1069,26 +1204,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeWidth && resizeHeight) { // bottom left corner - if (x >= winrect.left && x < winrect.left + border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMLEFT; } // bottom right corner - if (x < winrect.right && x >= winrect.right - border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMRIGHT; } // top left corner - if (x >= winrect.left && x < winrect.left + border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPLEFT; } // top right corner - if (x < winrect.right && x >= winrect.right - border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPRIGHT; } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index b9f21b08d..02101cb6d 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -75,7 +75,6 @@ public: bool applyLastBoundsCheck(); float scale() const override; - float qtFontScale() const; /// @returns true if the window is the top-most window. /// Either #setTopMost was called or the `TopMost` flag is set which overrides this @@ -132,7 +131,6 @@ private: void drawCustomWindowFrame(QPainter &painter); void onFocusLost(); - bool handleDPICHANGED(MSG *msg); bool handleSHOWWINDOW(MSG *msg); bool handleSIZE(MSG *msg); bool handleMOVE(MSG *msg); @@ -149,8 +147,6 @@ private: bool frameless_; bool shown_ = false; FlagsEnum flags_; - float nativeScale_ = 1; - bool isResizeFixing_ = false; bool isTopMost_ = false; struct { @@ -168,6 +164,7 @@ private: widgets::BoundsChecking lastBoundsCheckMode_ = widgets::BoundsChecking::Off; #ifdef USEWINSDK + void updateRealSize(); /// @brief Returns the HWND of this window if it has one /// /// A QWidget only has an HWND if it has been created. Before that, @@ -193,6 +190,10 @@ private: QTimer useNextBounds_; bool isNotMinimizedOrMaximized_{}; bool lastEventWasNcMouseMove_ = false; + /// The real bounds of the window as returned by + /// GetWindowRect. Used for drawing. + QRect realBounds_; + bool isMaximized_ = false; #endif pajlada::Signals::SignalHolder connections_; diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index 37bd9df36..1d3e7067f 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -88,23 +88,10 @@ void Label::paintEvent(QPaintEvent *) { QPainter painter(this); - qreal deviceDpi = -#ifdef Q_OS_WIN - this->devicePixelRatioF(); -#else - 1.0; -#endif - QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.01F, static_cast(this->logicalDpiX() * deviceDpi))); - painter.setFont(getIApp()->getFonts()->getFont( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.02F, static_cast(this->logicalDpiX() * deviceDpi)))); + this->getFontStyle(), this->scale()); + painter.setFont( + getIApp()->getFonts()->getFont(this->getFontStyle(), this->scale())); int offset = this->getOffset(); diff --git a/src/widgets/TooltipEntryWidget.cpp b/src/widgets/TooltipEntryWidget.cpp index 7ef7274e3..0f0ac5336 100644 --- a/src/widgets/TooltipEntryWidget.cpp +++ b/src/widgets/TooltipEntryWidget.cpp @@ -86,6 +86,7 @@ bool TooltipEntryWidget::refreshPixmap() this->attemptRefresh_ = true; return false; } + pixmap->setDevicePixelRatio(this->devicePixelRatio()); if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 62d459e22..8b56f4903 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -47,7 +47,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) this->resize(915, 600); this->themeChangedEvent(); - this->scaleChangedEvent(this->scale()); + QFile styleFile(":/qss/settings.qss"); + styleFile.open(QFile::ReadOnly); + QString stylesheet = QString::fromUtf8(styleFile.readAll()); + this->setStyleSheet(stylesheet); this->initUi(); this->addTabs(); @@ -396,25 +399,19 @@ void SettingsDialog::refresh() void SettingsDialog::scaleChangedEvent(float newDpi) { - QFile file(":/qss/settings.qss"); - file.open(QFile::ReadOnly); - QString styleSheet = QLatin1String(file.readAll()); - styleSheet.replace("", QString::number(int(14 * newDpi))); - styleSheet.replace("", QString::number(int(14 * newDpi))); + assert(newDpi == 1.F && + "Scaling is disabled for the settings dialog - its scale should " + "always be 1"); for (SettingsDialogTab *tab : this->tabs_) { - tab->setFixedHeight(int(30 * newDpi)); + tab->setFixedHeight(30); } - this->setStyleSheet(styleSheet); - if (this->ui_.tabContainerContainer) { - this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi)); + this->ui_.tabContainerContainer->setFixedWidth(150); } - - this->dpi_ = newDpi; } void SettingsDialog::themeChangedEvent() diff --git a/src/widgets/helper/Button.cpp b/src/widgets/helper/Button.cpp index 08dde78e1..057432666 100644 --- a/src/widgets/helper/Button.cpp +++ b/src/widgets/helper/Button.cpp @@ -8,26 +8,44 @@ #include #include -namespace chatterino { namespace { - // returns a new resized image or the old one if the size didn't change - auto resizePixmap(const QPixmap ¤t, const QPixmap resized, - const QSize &size) -> QPixmap +QSizeF deviceIndependentSize(const QPixmap &pixmap) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0) + return QSizeF(pixmap.width(), pixmap.height()) / pixmap.devicePixelRatio(); +#else + return pixmap.deviceIndependentSize(); +#endif +} + +/** + * Resizes a pixmap to a desired size. + * Does nothing if the target pixmap is already sized correctly. + * + * @param target The target pixmap. + * @param source The unscaled pixmap. + * @param size The desired device independent size. + * @param dpr The device pixel ratio of the target area. The size of the target in pixels will be `size * dpr`. + */ +void resizePixmap(QPixmap &target, const QPixmap &source, const QSize &size, + qreal dpr) +{ + if (deviceIndependentSize(target) == size) { - if (resized.size() == size) - { - return resized; - } - else - { - return current.scaled(size, Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - } + return; } + QPixmap resized = source; + resized.setDevicePixelRatio(dpr); + target = resized.scaled(size * dpr, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); +} + } // namespace +namespace chatterino { + Button::Button(BaseWidget *parent) : BaseWidget(parent) { @@ -47,6 +65,12 @@ void Button::setMouseEffectColor(std::optional color) void Button::setPixmap(const QPixmap &_pixmap) { + // Avoid updates if the pixmap didn't change + if (_pixmap.cacheKey() == this->pixmap_.cacheKey()) + { + return; + } + this->pixmap_ = _pixmap; this->resizedPixmap_ = {}; this->update(); @@ -158,8 +182,8 @@ void Button::paintButton(QPainter &painter) QRect rect = this->rect(); - this->resizedPixmap_ = - resizePixmap(this->pixmap_, this->resizedPixmap_, rect.size()); + resizePixmap(this->resizedPixmap_, this->pixmap_, rect.size(), + this->devicePixelRatio()); int margin = this->height() < 22 * this->scale() ? 3 : 6; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 819b7d0c0..ed636cca4 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -615,7 +615,7 @@ void ChannelView::scaleChangedEvent(float scale) if (this->goToBottom_) { - auto factor = this->qtFontScale(); + auto factor = this->scale(); #ifdef Q_OS_MACOS factor = scale * 80.F / std::max( @@ -703,8 +703,10 @@ void ChannelView::layoutVisibleMessages( { const auto &message = messages[i]; - redrawRequired |= message->layout(layoutWidth, this->scale(), flags, - this->bufferInvalidationQueued_); + redrawRequired |= message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), + flags, this->bufferInvalidationQueued_); y += message->getHeight(); } @@ -738,7 +740,10 @@ void ChannelView::updateScrollbar( { auto *message = messages[i].get(); - message->layout(layoutWidth, this->scale(), flags, false); + message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), flags, + false); h -= message->getHeight(); @@ -1720,9 +1725,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i - 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i - 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i - 1]->getHeight(); } @@ -1755,9 +1762,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i + 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i + 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i + 1]->getHeight(); diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index da04562fa..f7cb2b3a0 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -27,15 +27,6 @@ namespace chatterino { namespace { - qreal deviceDpi(QWidget *widget) - { -#ifdef Q_OS_WIN - return widget->devicePixelRatioF(); -#else - return 1.0; -#endif - } - // Translates the given rectangle by an amount in the direction to appear like the tab is selected. // For example, if location is Top, the rectangle will be translated in the negative Y direction, // or "up" on the screen, by amount. @@ -196,8 +187,8 @@ int NotebookTab::normalTabWidth() float scale = this->scale(); int width; - auto metrics = getIApp()->getFonts()->getFontMetrics( - FontStyle::UiTabs, float(qreal(this->scale()) * deviceDpi(this))); + QFontMetrics metrics = + getIApp()->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); if (this->hasXButton()) { @@ -439,11 +430,9 @@ void NotebookTab::paintEvent(QPaintEvent *) QPainter painter(this); float scale = this->scale(); - auto div = std::max(0.01f, this->logicalDpiX() * deviceDpi(this)); - painter.setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, scale * 96.f / div)); + painter.setFont(app->getFonts()->getFont(FontStyle::UiTabs, scale)); QFontMetrics metrics = - app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale * 96.f / div); + app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); int height = int(scale * NOTEBOOK_TAB_HEIGHT); diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index ab9a294c9..8533b87b8 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -63,7 +63,7 @@ public: builder.append( std::make_unique(text, MessageElementFlag::Text)); this->layout = std::make_unique(builder.release()); - this->layout->layout(WIDTH, 1, MessageElementFlag::Text, false); + this->layout->layout(WIDTH, 1, 1, MessageElementFlag::Text, false); } MockApplication mockApplication; From 3d5acff907806c75095068d6ed84c9ccd2f78300 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 12 May 2024 14:37:47 +0200 Subject: [PATCH 69/71] fix: update color of usernames & boldness of usernames on the fly (#5300) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 5 +- src/messages/MessageElement.cpp | 32 +++++ src/messages/MessageElement.hpp | 44 ++++++- .../layouts/MessageLayoutContainer.cpp | 4 +- src/providers/twitch/TwitchMessageBuilder.cpp | 120 ++++++------------ src/singletons/WindowManager.cpp | 10 +- 7 files changed, 120 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef547e8bc..6a9654f3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868) - Minor: Add option to customise Moderation buttons with images. (#5369) +- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index e17194549..1ffd5ba28 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -763,10 +763,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) auto &&textColor = this->textColor_; if (string.startsWith('@')) { - this->emplace(string, MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold); - this->emplace(string, MessageElementFlag::NonBoldUsername, - textColor); + this->emplace(string, textColor, textColor); } else { diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 3d5ab64e7..17de12706 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -703,6 +703,38 @@ Link LinkElement::getLink() const return {Link::Url, this->linkInfo_.url()}; } +MentionElement::MentionElement(const QString &name, MessageColor fallbackColor_, + MessageColor userColor_) + : TextElement(name, {MessageElementFlag::Text, MessageElementFlag::Mention}) + , fallbackColor(fallbackColor_) + , userColor(userColor_) +{ +} + +void MentionElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (getSettings()->colorUsernames) + { + this->color_ = this->userColor; + } + else + { + this->color_ = this->fallbackColor; + } + + if (getSettings()->boldUsernames) + { + this->style_ = FontStyle::ChatMediumBold; + } + else + { + this->style_ = FontStyle::ChatMedium; + } + + TextElement::addToContainer(container, flags); +} + // TIMESTAMP TimestampElement::TimestampElement(QTime time) : MessageElement(MessageElementFlag::Timestamp) diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index b57bab752..2c1e98f4e 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -133,9 +133,10 @@ enum class MessageElementFlag : int64_t { // needed Collapsed = (1LL << 26), - // used for dynamic bold usernames - BoldUsername = (1LL << 27), - NonBoldUsername = (1LL << 28), + // A mention of a username that isn't the author of the message + Mention = (1LL << 27), + + // Unused = (1LL << 28), // used to check if links should be lowercased LowercaseLinks = (1LL << 29), @@ -236,7 +237,6 @@ public: protected: QStringList words_; -private: MessageColor color_; FontStyle style_; }; @@ -301,6 +301,42 @@ private: QStringList original_; }; +/** + * @brief Contains a username mention. + * + * Examples of mentions: + * V + * 13:37 pajlada: hello @forsen + * + * V V + * 13:37 The moderators of this channel are: forsen, nuuls + */ +class MentionElement : public TextElement +{ +public: + MentionElement(const QString &name, MessageColor fallbackColor_, + MessageColor userColor_); + ~MentionElement() override = default; + MentionElement(const MentionElement &) = delete; + MentionElement(MentionElement &&) = delete; + MentionElement &operator=(const MentionElement &) = delete; + MentionElement &operator=(MentionElement &&) = delete; + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + +private: + /** + * The color of the element in case the "Colorize @usernames" is disabled + **/ + MessageColor fallbackColor; + + /** + * The color of the element in case the "Colorize @usernames" is enabled + **/ + MessageColor userColor; +}; + // contains emote data and will pick the emote based on : // a) are images for the emote type enabled // b) which size it wants diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 15a7d71af..e5e53f360 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -756,9 +756,7 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex) const auto neutral = isNeutral(element->getText()); const auto neutralOrUsername = - neutral || - element->getFlags().hasAny({MessageElementFlag::BoldUsername, - MessageElementFlag::NonBoldUsername}); + neutral || element->getFlags().has(MessageElementFlag::Mention); if (neutral && ((this->first == FirstWord::RTL && !this->wasPrevReversed_) || diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 524d0375d..3dfd39720 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -51,6 +51,8 @@ using namespace chatterino::literals; namespace { +const QColor AUTOMOD_USER_COLOR{"blue"}; + using namespace std::chrono_literals; const QString regexHelpString("(\\w+)[.,!?;:]*?$"); @@ -756,7 +758,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) QString username = match.captured(1); auto originalTextColor = textColor; - if (this->twitchChannel != nullptr && getSettings()->colorUsernames) + if (this->twitchChannel != nullptr) { if (auto userColor = this->twitchChannel->getUserColor(username); @@ -767,21 +769,17 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } auto prefixedUsername = '@' + username; - this->emplace(prefixedUsername, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) + auto remainder = string.remove(prefixedUsername); + this->emplace(prefixedUsername, originalTextColor, + textColor) ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); + ->setTrailingSpace(remainder.isEmpty()); - this->emplace(prefixedUsername, - MessageElementFlag::NonBoldUsername, - textColor) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(string.remove(prefixedUsername), - MessageElementFlag::Text, - originalTextColor); + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } return; } @@ -797,30 +795,23 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) { auto originalTextColor = textColor; - if (getSettings()->colorUsernames) + if (auto userColor = this->twitchChannel->getUserColor(username); + userColor.isValid()) { - if (auto userColor = - this->twitchChannel->getUserColor(username); - userColor.isValid()) - { - textColor = userColor; - } + textColor = userColor; } - this->emplace(username, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) + auto remainder = string.remove(username); + this->emplace(username, originalTextColor, + textColor) ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); + ->setTrailingSpace(remainder.isEmpty()); - this->emplace( - username, MessageElementFlag::NonBoldUsername, textColor) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(string.remove(username), - MessageElementFlag::Text, - originalTextColor); + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } return; } @@ -1821,7 +1812,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, MessageColor color = MessageColor::System; - if (tc && getSettings()->colorUsernames) + if (tc) { if (auto userColor = tc->getUserColor(username); userColor.isValid()) @@ -1830,14 +1821,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, } } - builder - ->emplace(username, MessageElementFlag::BoldUsername, - color, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - builder - ->emplace(username, - MessageElementFlag::NonBoldUsername, color) + builder->emplace(username, MessageColor::System, color) ->setLink({Link::UserInfo, username}) ->setTrailingSpace(false); } @@ -1873,7 +1857,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage( MessageColor color = MessageColor::System; - if (tc && getSettings()->colorUsernames) + if (tc) { if (auto userColor = tc->getUserColor(user.userLogin); userColor.isValid()) @@ -1883,14 +1867,8 @@ void TwitchMessageBuilder::listOfUsersSystemMessage( } builder - ->emplace(user.userName, - MessageElementFlag::BoldUsername, color, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, user.userLogin}) - ->setTrailingSpace(false); - builder - ->emplace(user.userName, - MessageElementFlag::NonBoldUsername, color) + ->emplace(user.userName, MessageColor::System, + color) ->setLink({Link::UserInfo, user.userLogin}) ->setTrailingSpace(false); } @@ -1960,12 +1938,8 @@ MessagePtr TwitchMessageBuilder::makeAutomodInfoMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); + builder.emplace("AutoMod:", AUTOMOD_USER_COLOR, + AUTOMOD_USER_COLOR); switch (action.type) { case AutomodInfoAction::OnHold: { @@ -2019,12 +1993,8 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); + builder2.emplace("AutoMod:", AUTOMOD_USER_COLOR, + AUTOMOD_USER_COLOR); // AutoMod header message builder.emplace( ("Held a message for reason: " + action.reason + @@ -2072,14 +2042,8 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( // sender username builder2 - .emplace( - action.target.displayName + ":", MessageElementFlag::BoldUsername, - MessageColor(action.target.color), FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.target.login}); - builder2 - .emplace(action.target.displayName + ":", - MessageElementFlag::NonBoldUsername, - MessageColor(action.target.color)) + .emplace(action.target.displayName + ":", + MessageColor::Text, action.target.color) ->setLink({Link::UserInfo, action.target.login}); // sender's message caught by AutoMod builder2.emplace(action.message, MessageElementFlag::Text, @@ -2275,17 +2239,9 @@ std::pair TwitchMessageBuilder::makeLowTrustUserMessage( appendBadges(&builder2, action.senderBadges, {}, twitchChannel); // sender username - builder2 - .emplace(action.suspiciousUserDisplayName + ":", - MessageElementFlag::BoldUsername, - MessageColor(action.suspiciousUserColor), - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.suspiciousUserLogin}); - builder2 - .emplace(action.suspiciousUserDisplayName + ":", - MessageElementFlag::NonBoldUsername, - MessageColor(action.suspiciousUserColor)) - ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder2.emplace(action.suspiciousUserDisplayName + ":", + MessageColor::Text, + action.suspiciousUserColor); // sender's message caught by AutoMod for (const auto &fragment : action.fragments) diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 56bea50ae..63bc63b36 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -108,7 +108,6 @@ WindowManager::WindowManager(const Paths &paths) this->wordFlagsListener_.addSetting(settings->showBadgesFfz); this->wordFlagsListener_.addSetting(settings->showBadgesSevenTV); this->wordFlagsListener_.addSetting(settings->enableEmoteImages); - this->wordFlagsListener_.addSetting(settings->boldUsernames); this->wordFlagsListener_.addSetting(settings->lowercaseDomains); this->wordFlagsListener_.addSetting(settings->showReplyButton); this->wordFlagsListener_.setCB([this] { @@ -182,8 +181,6 @@ void WindowManager::updateWordTypeMask() // misc flags.set(MEF::AlwaysShow); flags.set(MEF::Collapsed); - flags.set(settings->boldUsernames ? MEF::BoldUsername - : MEF::NonBoldUsername); flags.set(MEF::LowercaseLinks, settings->lowercaseDomains); flags.set(MEF::ChannelPointReward); @@ -422,6 +419,13 @@ void WindowManager::initialize(Settings &settings, const Paths &paths) this->forceLayoutChannelViews(); }); + settings.colorUsernames.connect([this](auto, auto) { + this->forceLayoutChannelViews(); + }); + settings.boldUsernames.connect([this](auto, auto) { + this->forceLayoutChannelViews(); + }); + this->initialized_ = true; } From 2ad45bc2880192f80614a5f6a198e87151ff37e2 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 22:46:26 +0200 Subject: [PATCH 70/71] fix: don't use DPI aware functions on Qt 5 (Windows 7/8) (#5391) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9654f3f..0e6b65f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unversioned - Major: Release plugins alpha. (#5288) -- Major: Improve high-DPI support on Windows. (#4868) +- Major: Improve high-DPI support on Windows. (#4868, #5391) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 5a1df70af..08a13007a 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -121,12 +121,19 @@ RECT windowBordersFor(HWND hwnd, bool isMaximized) auto addBorders = isMaximized || isWindows11OrGreater(); if (addBorders) { + // GetDpiForWindow and GetSystemMetricsForDpi are only supported on + // Windows 10 and later. Qt 6 requires Windows 10. +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto dpi = GetDpiForWindow(hwnd); +# endif + auto systemMetric = [&](auto index) { +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (dpi != 0) { return GetSystemMetricsForDpi(index, dpi); } +# endif return GetSystemMetrics(index); }; From fdecb4a39f3e0340fe555797030454986b50be78 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 13 May 2024 20:00:50 +0200 Subject: [PATCH 71/71] revert: use max(minimum, min(bottom, value)) over clamp(..) (#5393) --- CHANGELOG.md | 2 +- src/widgets/Scrollbar.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e6b65f61..66d5e9dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) -- Dev: Refactor and document `Scrollbar`. (#5334) +- Dev: Refactor and document `Scrollbar`. (#5334, #5393) ## 2.5.1 diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index e1492b673..827ea645b 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -153,7 +153,9 @@ void Scrollbar::setPageSize(qreal value) void Scrollbar::setDesiredValue(qreal value, bool animated) { - value = std::clamp(value, this->minimum_, this->getBottom()); + // this can't use std::clamp, because minimum_ < getBottom() isn't always + // true, which is a precondition for std::clamp + value = std::max(this->minimum_, std::min(this->getBottom(), value)); if (areClose(this->currentValue_, value)) { // value has not changed