diff --git a/CHANGELOG.md b/CHANGELOG.md index 346ef577c..95a282ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Dev: Fix clang-tidy `cppcoreguidelines-pro-type-member-init` warnings. (#4426) - Dev: Immediate layout for invisible `ChannelView`s is skipped. (#4811) - Dev: Refactor `Image` & Image's `Frames`. (#4773) +- Dev: Clarify signal connection lifetimes where applicable. (#4818) ## 2.4.5 diff --git a/src/Application.cpp b/src/Application.cpp index 39f4ebf7f..bd3b5f0f6 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -97,7 +97,9 @@ Application::Application(Settings &_settings, Paths &_paths) { this->instance = this; - this->fonts->fontChanged.connect([this]() { + // We can safely ignore this signal's connection since the Application will always + // be destroyed after fonts + std::ignore = this->fonts->fontChanged.connect([this]() { this->windows->layoutChannelViews(); }); } @@ -188,16 +190,23 @@ int Application::run(QApplication &qtApp) Updates::instance().checkForUpdates(); }, false); - getSettings()->moderationActions.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); - getSettings()->highlightedMessages.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); - getSettings()->highlightedUsers.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); + // We can safely ignore the signal connections since Application will always live longer than + // everything else, including settings. right? + // NOTE: SETTINGS_LIFETIME + std::ignore = + getSettings()->moderationActions.delayedItemsChanged.connect([this] { + this->windows->forceLayoutChannelViews(); + }); + + std::ignore = + getSettings()->highlightedMessages.delayedItemsChanged.connect([this] { + this->windows->forceLayoutChannelViews(); + }); + std::ignore = + getSettings()->highlightedUsers.delayedItemsChanged.connect([this] { + this->windows->forceLayoutChannelViews(); + }); getSettings()->removeSpacesBetweenEmotes.connect([this] { this->windows->forceLayoutChannelViews(); @@ -279,7 +288,9 @@ void Application::initNm(Paths &paths) void Application::initPubSub() { - this->twitch->pubsub->signals_.moderation.chatCleared.connect( + // We can safely ignore these signal connections since the twitch object will always + // be destroyed before the Application + std::ignore = this->twitch->pubsub->signals_.moderation.chatCleared.connect( [this](const auto &action) { auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); if (chan->isEmpty()) @@ -296,7 +307,7 @@ void Application::initPubSub() }); }); - this->twitch->pubsub->signals_.moderation.modeChanged.connect( + std::ignore = this->twitch->pubsub->signals_.moderation.modeChanged.connect( [this](const auto &action) { auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); if (chan->isEmpty()) @@ -322,28 +333,29 @@ void Application::initPubSub() }); }); - this->twitch->pubsub->signals_.moderation.moderationStateChanged.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } + std::ignore = + this->twitch->pubsub->signals_.moderation.moderationStateChanged + .connect([this](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } - QString text; + QString text; - text = QString("%1 %2 %3") - .arg(action.source.login, - (action.modded ? "modded" : "unmodded"), - action.target.login); + text = QString("%1 %2 %3") + .arg(action.source.login, + (action.modded ? "modded" : "unmodded"), + action.target.login); - auto msg = makeSystemMessage(text); - postToThread([chan, msg] { - chan->addMessage(msg); + auto msg = makeSystemMessage(text); + postToThread([chan, msg] { + chan->addMessage(msg); + }); }); - }); - this->twitch->pubsub->signals_.moderation.userBanned.connect( + std::ignore = this->twitch->pubsub->signals_.moderation.userBanned.connect( [&](const auto &action) { auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); @@ -358,208 +370,218 @@ void Application::initPubSub() chan->addOrReplaceTimeout(msg.release()); }); }); - this->twitch->pubsub->signals_.moderation.messageDeleted.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + std::ignore = + this->twitch->pubsub->signals_.moderation.messageDeleted.connect( + [&](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty() || getSettings()->hideDeletionActions) - { - return; - } - - MessageBuilder msg; - TwitchMessageBuilder::deletionMessage(action, &msg); - msg->flags.set(MessageFlag::PubSub); - - postToThread([chan, msg = msg.release()] { - auto replaced = false; - LimitedQueueSnapshot snapshot = - chan->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - // without parens it doesn't build on windows - int end = (std::max)(0, snapshotLength - 200); - - for (int i = snapshotLength - 1; i >= end; --i) + if (chan->isEmpty() || getSettings()->hideDeletionActions) { - auto &s = snapshot[i]; - if (!s->flags.has(MessageFlag::PubSub) && - s->timeoutUser == msg->timeoutUser) - { - chan->replaceMessage(s, msg); - replaced = true; - break; - } + return; } - if (!replaced) + + MessageBuilder msg; + TwitchMessageBuilder::deletionMessage(action, &msg); + msg->flags.set(MessageFlag::PubSub); + + postToThread([chan, msg = msg.release()] { + auto replaced = false; + LimitedQueueSnapshot snapshot = + chan->getMessageSnapshot(); + int snapshotLength = snapshot.size(); + + // without parens it doesn't build on windows + int end = (std::max)(0, snapshotLength - 200); + + for (int i = snapshotLength - 1; i >= end; --i) + { + auto &s = snapshot[i]; + if (!s->flags.has(MessageFlag::PubSub) && + s->timeoutUser == msg->timeoutUser) + { + chan->replaceMessage(s, msg); + replaced = true; + break; + } + } + if (!replaced) + { + chan->addMessage(msg); + } + }); + }); + + std::ignore = + this->twitch->pubsub->signals_.moderation.userUnbanned.connect( + [&](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { chan->addMessage(msg); + }); + }); + + std::ignore = + this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect( + [&](const auto &msg, const QString &channelID) { + auto chan = this->twitch->getChannelOrEmptyByID(channelID); + if (chan->isEmpty()) + { + return; } - }); - }); - this->twitch->pubsub->signals_.moderation.userUnbanned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - auto msg = MessageBuilder(action).release(); - - postToThread([chan, msg] { - chan->addMessage(msg); - }); - }); - - this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect( - [&](const auto &msg, const QString &channelID) { - auto chan = this->twitch->getChannelOrEmptyByID(channelID); - if (chan->isEmpty()) - { - return; - } - - switch (msg.type) - { - case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: { - if (msg.status == "PENDING") - { - AutomodAction action(msg.data, channelID); - action.reason = QString("%1 level %2") - .arg(msg.contentCategory) - .arg(msg.contentLevel); - - action.msgID = msg.messageID; - action.message = msg.messageText; - - // this message also contains per-word automod data, which could be implemented - - // extract sender data manually because Twitch loves not being consistent - QString senderDisplayName = - msg.senderUserDisplayName; // Might be transformed later - bool hasLocalizedName = false; - if (!msg.senderUserDisplayName.isEmpty()) + switch (msg.type) + { + case PubSubAutoModQueueMessage::Type:: + AutoModCaughtMessage: { + if (msg.status == "PENDING") { - // check for non-ascii display names - if (QString::compare(msg.senderUserDisplayName, - msg.senderUserLogin, - Qt::CaseInsensitive) != 0) + AutomodAction action(msg.data, channelID); + action.reason = QString("%1 level %2") + .arg(msg.contentCategory) + .arg(msg.contentLevel); + + action.msgID = msg.messageID; + action.message = msg.messageText; + + // this message also contains per-word automod data, which could be implemented + + // extract sender data manually because Twitch loves not being consistent + QString senderDisplayName = + msg.senderUserDisplayName; // Might be transformed later + bool hasLocalizedName = false; + if (!msg.senderUserDisplayName.isEmpty()) { - hasLocalizedName = true; - } - } - QColor senderColor = msg.senderUserChatColor; - QString senderColor_; - if (!senderColor.isValid() && - getSettings()->colorizeNicknames) - { - // color may be not present if user is a grey-name - senderColor = getRandomColor(msg.senderUserID); - } - - // handle username style based on prefered setting - switch (getSettings()->usernameDisplayMode.getValue()) - { - case UsernameDisplayMode::Username: { - if (hasLocalizedName) + // check for non-ascii display names + if (QString::compare(msg.senderUserDisplayName, + msg.senderUserLogin, + Qt::CaseInsensitive) != 0) { - senderDisplayName = msg.senderUserLogin; + hasLocalizedName = true; } - break; } - case UsernameDisplayMode::LocalizedName: { - break; + QColor senderColor = msg.senderUserChatColor; + QString senderColor_; + if (!senderColor.isValid() && + getSettings()->colorizeNicknames) + { + // color may be not present if user is a grey-name + senderColor = getRandomColor(msg.senderUserID); } - case UsernameDisplayMode:: - UsernameAndLocalizedName: { - if (hasLocalizedName) - { - senderDisplayName = QString("%1(%2)").arg( - msg.senderUserLogin, - msg.senderUserDisplayName); - } - break; - } - } - action.target = - ActionUser{msg.senderUserID, msg.senderUserLogin, - senderDisplayName, senderColor}; - postToThread([chan, action] { - const auto p = makeAutomodMessage(action); - chan->addMessage(p.first); - chan->addMessage(p.second); - }); + // handle username style based on prefered setting + switch ( + getSettings()->usernameDisplayMode.getValue()) + { + case UsernameDisplayMode::Username: { + if (hasLocalizedName) + { + senderDisplayName = msg.senderUserLogin; + } + break; + } + case UsernameDisplayMode::LocalizedName: { + break; + } + case UsernameDisplayMode:: + UsernameAndLocalizedName: { + if (hasLocalizedName) + { + senderDisplayName = + QString("%1(%2)").arg( + msg.senderUserLogin, + msg.senderUserDisplayName); + } + break; + } + } + + action.target = ActionUser{ + msg.senderUserID, msg.senderUserLogin, + senderDisplayName, senderColor}; + postToThread([chan, action] { + const auto p = makeAutomodMessage(action); + chan->addMessage(p.first); + chan->addMessage(p.second); + }); + } + // "ALLOWED" and "DENIED" statuses remain unimplemented + // They are versions of automod_message_(denied|approved) but for mods. } - // "ALLOWED" and "DENIED" statuses remain unimplemented - // They are versions of automod_message_(denied|approved) but for mods. + break; + + case PubSubAutoModQueueMessage::Type::INVALID: + default: { + } + break; } - break; + }); - case PubSubAutoModQueueMessage::Type::INVALID: - default: { + std::ignore = + this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect( + [&](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; } - break; - } - }); - this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - const auto p = makeAutomodMessage(action); - chan->addMessage(p.first); - chan->addMessage(p.second); + postToThread([chan, action] { + const auto p = makeAutomodMessage(action); + chan->addMessage(p.first); + chan->addMessage(p.second); + }); }); - }); - this->twitch->pubsub->signals_.moderation.automodUserMessage.connect( - [&](const auto &action) { - // This condition has been set up to execute isInStreamerMode() as the last thing - // as it could end up being expensive. - if (getSettings()->streamerModeHideModActions && isInStreamerMode()) - { - return; - } - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + std::ignore = + this->twitch->pubsub->signals_.moderation.automodUserMessage.connect( + [&](const auto &action) { + // This condition has been set up to execute isInStreamerMode() as the last thing + // as it could end up being expensive. + if (getSettings()->streamerModeHideModActions && + isInStreamerMode()) + { + return; + } + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } + if (chan->isEmpty()) + { + return; + } - auto msg = MessageBuilder(action).release(); + auto msg = MessageBuilder(action).release(); - postToThread([chan, msg] { - chan->addMessage(msg); + postToThread([chan, msg] { + chan->addMessage(msg); + }); + chan->deleteMessage(msg->id); }); - chan->deleteMessage(msg->id); - }); - this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + std::ignore = + this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect( + [&](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } + if (chan->isEmpty()) + { + return; + } - postToThread([chan, action] { - const auto p = makeAutomodInfoMessage(action); - chan->addMessage(p); + postToThread([chan, action] { + const auto p = makeAutomodInfoMessage(action); + chan->addMessage(p); + }); }); - }); - this->twitch->pubsub->signals_.pointReward.redeemed.connect( + std::ignore = this->twitch->pubsub->signals_.pointReward.redeemed.connect( [&](auto &data) { QString channelId = data.value("channel_id").toString(); if (channelId.isEmpty()) @@ -614,7 +636,9 @@ void Application::initBttvLiveUpdates() return; } - this->twitch->bttvLiveUpdates->signals_.emoteAdded.connect( + // We can safely ignore these signal connections since the twitch object will always + // be destroyed before the Application + std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteAdded.connect( [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); @@ -625,7 +649,7 @@ void Application::initBttvLiveUpdates() } }); }); - this->twitch->bttvLiveUpdates->signals_.emoteUpdated.connect( + std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteUpdated.connect( [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); @@ -636,7 +660,7 @@ void Application::initBttvLiveUpdates() } }); }); - this->twitch->bttvLiveUpdates->signals_.emoteRemoved.connect( + std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteRemoved.connect( [&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); @@ -659,7 +683,9 @@ void Application::initSeventvEventAPI() return; } - this->twitch->seventvEventAPI->signals_.emoteAdded.connect( + // We can safely ignore these signal connections since the twitch object will always + // be destroyed before the Application + std::ignore = this->twitch->seventvEventAPI->signals_.emoteAdded.connect( [&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( @@ -668,7 +694,7 @@ void Application::initSeventvEventAPI() }); }); }); - this->twitch->seventvEventAPI->signals_.emoteUpdated.connect( + std::ignore = this->twitch->seventvEventAPI->signals_.emoteUpdated.connect( [&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( @@ -677,7 +703,7 @@ void Application::initSeventvEventAPI() }); }); }); - this->twitch->seventvEventAPI->signals_.emoteRemoved.connect( + std::ignore = this->twitch->seventvEventAPI->signals_.emoteRemoved.connect( [&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( @@ -686,7 +712,7 @@ void Application::initSeventvEventAPI() }); }); }); - this->twitch->seventvEventAPI->signals_.userUpdated.connect( + std::ignore = this->twitch->seventvEventAPI->signals_.userUpdated.connect( [&](const auto &data) { this->twitch->forEachSeventvUser(data.userID, [data](TwitchChannel &chan) { diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 88cadaa03..96ab49ef3 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -106,7 +106,7 @@ void Channel::addMessage(MessagePtr message, if (this->messages_.pushBack(message, deleted)) { - this->messageRemovedFromStart.invoke(deleted); + this->messageRemovedFromStart(deleted); } this->messageAppended.invoke(message, overridingFlags); @@ -353,6 +353,10 @@ void Channel::onConnected() { } +void Channel::messageRemovedFromStart(const MessagePtr &msg) +{ +} + // // Indirect channel // diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 3c2a82017..40c97306c 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -52,7 +52,6 @@ public: pajlada::Signals::Signal sendReplySignal; - pajlada::Signals::Signal messageRemovedFromStart; pajlada::Signals::Signal> messageAppended; pajlada::Signals::Signal &> messagesAddedAtStart; @@ -114,6 +113,7 @@ public: protected: virtual void onConnected(); + virtual void messageRemovedFromStart(const MessagePtr &msg); private: const QString name_; diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 0ba3bc959..3f8eeb0d3 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -1,4 +1,4 @@ -#include "AccountController.hpp" +#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/Account.hpp" #include "controllers/accounts/AccountModel.hpp" @@ -10,22 +10,27 @@ namespace chatterino { AccountController::AccountController() : accounts_(SharedPtrElementLess{}) { - this->twitch.accounts.itemInserted.connect([this](const auto &args) { - this->accounts_.insert(std::dynamic_pointer_cast(args.item)); - }); + // These signal connections can safely be ignored since the twitch object + // will always be destroyed before the AccountController + std::ignore = + this->twitch.accounts.itemInserted.connect([this](const auto &args) { + this->accounts_.insert( + std::dynamic_pointer_cast(args.item)); + }); - this->twitch.accounts.itemRemoved.connect([this](const auto &args) { - if (args.caller != this) - { - auto &accs = this->twitch.accounts.raw(); - auto it = std::find(accs.begin(), accs.end(), args.item); - assert(it != accs.end()); + std::ignore = + this->twitch.accounts.itemRemoved.connect([this](const auto &args) { + if (args.caller != this) + { + auto &accs = this->twitch.accounts.raw(); + auto it = std::find(accs.begin(), accs.end(), args.item); + assert(it != accs.end()); - this->accounts_.removeAt(it - accs.begin(), this); - } - }); + this->accounts_.removeAt(it - accs.begin(), this); + } + }); - this->accounts_.itemRemoved.connect([this](const auto &args) { + std::ignore = this->accounts_.itemRemoved.connect([this](const auto &args) { switch (args.item->getProviderId()) { case ProviderId::Twitch: { diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 9ce784bae..51ac02fa4 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -587,8 +587,10 @@ void CommandController::initialize(Settings &, Paths &paths) this->maxSpaces_ = maxSpaces; }; - this->items.itemInserted.connect(addFirstMatchToMap); - this->items.itemRemoved.connect(addFirstMatchToMap); + // We can safely ignore these signal connections since items will be destroyed + // before CommandController + std::ignore = this->items.itemInserted.connect(addFirstMatchToMap); + std::ignore = this->items.itemRemoved.connect(addFirstMatchToMap); // Initialize setting manager for commands.json auto path = combinePath(paths.settingsDirectory, "commands.json"); @@ -604,7 +606,7 @@ void CommandController::initialize(Settings &, Paths &paths) // Update the setting when the vector of commands has been updated (most // likely from the settings dialog) - this->items.delayedItemsChanged.connect([this] { + std::ignore = this->items.delayedItemsChanged.connect([this] { this->commandsSetting_->setValue(this->items.raw()); }); diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 69da63b01..1fb06b2bd 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -36,9 +36,13 @@ void NotificationController::initialize(Settings &settings, Paths &paths) this->channelMap[Platform::Twitch].append(channelName); } - this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { - this->twitchSetting_.setValue(this->channelMap[Platform::Twitch].raw()); - }); + // We can safely ignore this signal connection since channelMap will always be destroyed + // before the NotificationController + std::ignore = + this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { + this->twitchSetting_.setValue( + this->channelMap[Platform::Twitch].raw()); + }); liveStatusTimer_ = new QTimer(); diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 77f0631ff..58fda00fd 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -50,7 +50,7 @@ AbstractIrcServer::AbstractIrcServer() this->writeConnection_->connectionLost, [this](bool timeout) { qCDebug(chatterinoIrc) << "Write connection reconnect requested. Timeout:" << timeout; - this->writeConnection_->smartReconnect.invoke(); + this->writeConnection_->smartReconnect(); }); // Listen to read connection message signals @@ -86,7 +86,7 @@ AbstractIrcServer::AbstractIrcServer() this->addGlobalSystemMessage( "Server connection timed out, reconnecting"); } - this->readConnection_->smartReconnect.invoke(); + this->readConnection_->smartReconnect(); }); } diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp index fd1b66851..26bd82824 100644 --- a/src/providers/irc/Irc2.cpp +++ b/src/providers/irc/Irc2.cpp @@ -88,7 +88,9 @@ void IrcServerData::setPassword(const QString &password) Irc::Irc() { - this->connections.itemInserted.connect([this](auto &&args) { + // We can safely ignore this signal connection since `connections` will always + // be destroyed before the Irc object + std::ignore = this->connections.itemInserted.connect([this](auto &&args) { // make sure only one id can only exist for one server assert(this->servers_.find(args.item.id) == this->servers_.end()); @@ -117,7 +119,9 @@ Irc::Irc() } }); - this->connections.itemRemoved.connect([this](auto &&args) { + // We can safely ignore this signal connection since `connections` will always + // be destroyed before the Irc object + std::ignore = this->connections.itemRemoved.connect([this](auto &&args) { // restore if (auto server = this->servers_.find(args.item.id); server != this->servers_.end()) @@ -141,7 +145,9 @@ Irc::Irc() } }); - this->connections.delayedItemsChanged.connect([this] { + // We can safely ignore this signal connection since `connections` will always + // be destroyed before the Irc object + std::ignore = this->connections.delayedItemsChanged.connect([this] { this->save(); }); } diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 7e4c45379..b06343f84 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -38,18 +38,6 @@ IrcConnection::IrcConnection(QObject *parent) } }); - // Schedule a reconnect that won't violate RECONNECT_MIN_INTERVAL - this->smartReconnect.connect([this] { - if (this->reconnectTimer_.isActive()) - { - return; - } - - auto delay = this->reconnectBackoff_.next(); - qCDebug(chatterinoIrc) << "Reconnecting in" << delay.count() << "ms"; - this->reconnectTimer_.start(delay); - }); - this->reconnectTimer_.setSingleShot(true); QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] { if (this->isConnected()) @@ -123,6 +111,19 @@ IrcConnection::~IrcConnection() this->disconnect(); } +void IrcConnection::smartReconnect() +{ + if (this->reconnectTimer_.isActive()) + { + // Ignore this reconnect request, we already have a reconnect request queued up + return; + } + + auto delay = this->reconnectBackoff_.next(); + qCDebug(chatterinoIrc) << "Reconnecting in" << delay.count() << "ms"; + this->reconnectTimer_.start(delay); +} + void IrcConnection::open() { this->expectConnectionLoss_ = false; diff --git a/src/providers/irc/IrcConnection2.hpp b/src/providers/irc/IrcConnection2.hpp index 1a31ea5cd..4c76161b8 100644 --- a/src/providers/irc/IrcConnection2.hpp +++ b/src/providers/irc/IrcConnection2.hpp @@ -20,7 +20,8 @@ public: pajlada::Signals::Signal connectionLost; // Request a reconnect with a minimum interval between attempts. - pajlada::Signals::NoArgSignal smartReconnect; + // This won't violate RECONNECT_MIN_INTERVAL + void smartReconnect(); virtual void open(); virtual void close(); diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 65a5f3a39..7d1d30246 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -20,7 +20,9 @@ TwitchAccountManager::TwitchAccountManager() currentUser->loadSeventvUserID(); }); - this->accounts.itemRemoved.connect([this](const auto &acc) { + // We can safely ignore this signal connection since accounts will always be removed + // before TwitchAccountManager + std::ignore = this->accounts.itemRemoved.connect([this](const auto &acc) { this->removeUser(acc.item.get()); }); } diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index f8bbd1eec..7de6f3333 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -94,25 +94,15 @@ TwitchChannel::TwitchChannel(const QString &name) })); this->refreshPubSub(); - this->userStateChanged.connect([this] { + // We can safely ignore this signal connection since it's a private signal, meaning + // it will only ever be invoked by TwitchChannel itself + std::ignore = this->userStateChanged.connect([this] { this->refreshPubSub(); }); - // room id loaded -> refresh live status - this->roomIdChanged.connect([this]() { - this->refreshPubSub(); - this->refreshBadges(); - this->refreshCheerEmotes(); - this->refreshFFZChannelEmotes(false); - this->refreshBTTVChannelEmotes(false); - this->refreshSevenTVChannelEmotes(false); - this->joinBttvChannel(); - this->listenSevenTVCosmetics(); - getIApp()->getTwitchLiveController()->add( - std::dynamic_pointer_cast(shared_from_this())); - }); - - this->connected.connect([this]() { + // We can safely ignore this signal connection this has no external dependencies - once the signal + // is destroyed, it will no longer be able to fire + std::ignore = this->connected.connect([this]() { if (this->roomId().isEmpty()) { // If we get a reconnected event when the room id is not set, we @@ -125,18 +115,7 @@ TwitchChannel::TwitchChannel(const QString &name) this->loadRecentMessagesReconnect(); }); - this->messageRemovedFromStart.connect([this](MessagePtr &msg) { - if (msg->replyThread) - { - if (msg->replyThread->liveCount(msg) == 0) - { - this->threads_.erase(msg->replyThread->rootId()); - } - } - }); - // timers - QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [this] { this->refreshChatters(); }); @@ -538,6 +517,20 @@ void TwitchChannel::showLoginMessage() this->addMessage(builder.release()); } +void TwitchChannel::roomIdChanged() +{ + this->refreshPubSub(); + this->refreshBadges(); + this->refreshCheerEmotes(); + this->refreshFFZChannelEmotes(false); + this->refreshBTTVChannelEmotes(false); + this->refreshSevenTVChannelEmotes(false); + this->joinBttvChannel(); + this->listenSevenTVCosmetics(); + getIApp()->getTwitchLiveController()->add( + std::dynamic_pointer_cast(shared_from_this())); +} + QString TwitchChannel::prepareMessage(const QString &message) const { auto app = getApp(); @@ -729,7 +722,7 @@ void TwitchChannel::setRoomId(const QString &id) if (*this->roomID_.accessConst() != id) { *this->roomID_.access() = id; - this->roomIdChanged.invoke(); + this->roomIdChanged(); this->loadRecentMessages(); } } @@ -1063,6 +1056,17 @@ bool TwitchChannel::tryReplaceLastLiveUpdateAddOrRemove( return true; } +void TwitchChannel::messageRemovedFromStart(const MessagePtr &msg) +{ + if (msg->replyThread) + { + if (msg->replyThread->liveCount(msg) == 0) + { + this->threads_.erase(msg->replyThread->rootId()); + } + } +} + const QString &TwitchChannel::subscriptionUrl() { return this->subscriptionUrl_; diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 2ef71de3a..a06299e23 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -191,8 +191,7 @@ public: const std::unordered_map> &threads() const; - // Signals - pajlada::Signals::NoArgSignal roomIdChanged; + // Only TwitchChannel may invoke this signal pajlada::Signals::NoArgSignal userStateChanged; /** @@ -242,8 +241,6 @@ private: QString actualDisplayName; } nameOptions; -private: - // Methods void refreshPubSub(); void refreshChatters(); void refreshBadges(); @@ -252,6 +249,11 @@ private: void loadRecentMessagesReconnect(); void cleanUpReplyThreads(); void showLoginMessage(); + + /// roomIdChanged is called whenever this channel's ID has been changed + /// This should only happen once per channel, whenever the ID goes from unset to set + void roomIdChanged(); + /** Joins (subscribes to) a Twitch channel for updates on BTTV. */ void joinBttvChannel() const; /** @@ -335,6 +337,8 @@ private: std::unordered_map> threads_; protected: + void messageRemovedFromStart(const MessagePtr &msg) override; + Atomic> bttvEmotes_; Atomic> ffzEmotes_; Atomic> seventvEmotes_; diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index bcefb58dd..ccef15ba8 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -133,21 +133,24 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection, std::shared_ptr TwitchIrcServer::createChannel( const QString &channelName) { - auto channel = - std::shared_ptr(new TwitchChannel(channelName)); + auto channel = std::make_shared(channelName); channel->initialize(); - channel->sendMessageSignal.connect( + // We can safely ignore these signal connections since the TwitchIrcServer is only + // ever destroyed when the full Application state is about to be destroyed, at which point + // no Channel's should live + // NOTE: CHANNEL_LIFETIME + std::ignore = channel->sendMessageSignal.connect( [this, channel = channel.get()](auto &chan, auto &msg, bool &sent) { this->onMessageSendRequested(channel, msg, sent); }); - channel->sendReplySignal.connect( + std::ignore = channel->sendReplySignal.connect( [this, channel = channel.get()](auto &chan, auto &msg, auto &replyId, bool &sent) { this->onReplySendRequested(channel, msg, replyId, sent); }); - return std::shared_ptr(channel); + return channel; } void TwitchIrcServer::privateMessageReceived( diff --git a/src/singletons/Logging.cpp b/src/singletons/Logging.cpp index cdf85b5ae..a83ecbd17 100644 --- a/src/singletons/Logging.cpp +++ b/src/singletons/Logging.cpp @@ -14,16 +14,21 @@ namespace chatterino { void Logging::initialize(Settings &settings, Paths & /*paths*/) { - settings.loggedChannels.delayedItemsChanged.connect([this, &settings]() { - this->threadGuard.guard(); + // We can safely ignore this signal connection since settings are only-ever destroyed + // on application exit + // NOTE: SETTINGS_LIFETIME + std::ignore = settings.loggedChannels.delayedItemsChanged.connect( + [this, &settings]() { + this->threadGuard.guard(); - this->onlyLogListedChannels.clear(); + this->onlyLogListedChannels.clear(); - for (const auto &loggedChannel : *settings.loggedChannels.readOnly()) - { - this->onlyLogListedChannels.insert(loggedChannel.channelName()); - } - }); + for (const auto &loggedChannel : + *settings.loggedChannels.readOnly()) + { + this->onlyLogListedChannels.insert(loggedChannel.channelName()); + } + }); } void Logging::addMessage(const QString &channelName, MessagePtr message, diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index cae51caee..1fc7bf292 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -344,9 +344,13 @@ void WindowManager::setEmotePopupPos(QPoint pos) void WindowManager::initialize(Settings &settings, Paths &paths) { + (void)paths; assertInGuiThread(); - getApp()->themes->repaintVisibleChatWidgets_.connect([this] { + // We can safely ignore this signal connection since both Themes and WindowManager + // share the Application state lifetime + // NOTE: APPLICATION_LIFETIME + std::ignore = getApp()->themes->repaintVisibleChatWidgets_.connect([this] { this->repaintVisibleChatWidgets(); }); diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index a8ff6bc64..7d687b667 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -1,4 +1,4 @@ -#include "InitUpdateButton.hpp" +#include "util/InitUpdateButton.hpp" #include "widgets/dialogs/UpdateDialog.hpp" #include "widgets/helper/Button.hpp" @@ -28,7 +28,11 @@ void initUpdateButton(Button &button, dialog->show(); dialog->raise(); - dialog->buttonClicked.connect([&button](auto buttonType) { + // We can safely ignore the signal connection because the dialog will always + // be destroyed before the button is destroyed, since it is destroyed on focus loss + // + // The button is either attached to a Notebook, or a Window frame + std::ignore = dialog->buttonClicked.connect([&button](auto buttonType) { switch (buttonType) { case UpdateDialog::Dismiss: { diff --git a/src/widgets/AccountSwitchWidget.cpp b/src/widgets/AccountSwitchWidget.cpp index 21afb7be2..01a0a7698 100644 --- a/src/widgets/AccountSwitchWidget.cpp +++ b/src/widgets/AccountSwitchWidget.cpp @@ -20,22 +20,23 @@ AccountSwitchWidget::AccountSwitchWidget(QWidget *parent) this->addItem(userName); } - app->accounts->twitch.userListUpdated.connect([=, this]() { - this->blockSignals(true); + this->managedConnections_.managedConnect( + app->accounts->twitch.userListUpdated, [=, this]() { + this->blockSignals(true); - this->clear(); + this->clear(); - this->addItem(ANONYMOUS_USERNAME_LABEL); + this->addItem(ANONYMOUS_USERNAME_LABEL); - for (const auto &userName : app->accounts->twitch.getUsernames()) - { - this->addItem(userName); - } + for (const auto &userName : app->accounts->twitch.getUsernames()) + { + this->addItem(userName); + } - this->refreshSelection(); + this->refreshSelection(); - this->blockSignals(false); - }); + this->blockSignals(false); + }); this->refreshSelection(); diff --git a/src/widgets/AccountSwitchWidget.hpp b/src/widgets/AccountSwitchWidget.hpp index 8d236766d..97a0fdd94 100644 --- a/src/widgets/AccountSwitchWidget.hpp +++ b/src/widgets/AccountSwitchWidget.hpp @@ -1,5 +1,7 @@ #pragma once +#include "pajlada/signals/signalholder.hpp" + #include namespace chatterino { @@ -15,6 +17,8 @@ public: private: void refreshSelection(); + + pajlada::Signals::SignalHolder managedConnections_; }; } // namespace chatterino diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 761fcaa12..9a5121f81 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -246,7 +246,9 @@ EmotePopup::EmotePopup(QWidget *parent) MessageElementFlag::Default, MessageElementFlag::AlwaysShow, MessageElementFlag::EmoteImages}); view->setEnableScrollingToBottom(false); - view->linkClicked.connect(clicked); + // We can safely ignore this signal connection since the ChannelView is deleted + // either when the notebook is deleted, or when our main layout is deleted. + std::ignore = view->linkClicked.connect(clicked); if (addToNotebook) { diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index 02c90c5b9..a9c95aaea 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -84,9 +84,12 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, this->ui_.threadView->setMinimumSize(400, 100); this->ui_.threadView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - this->ui_.threadView->mouseDown.connect([this](QMouseEvent *) { - this->giveFocus(Qt::MouseFocusReason); - }); + // We can safely ignore this signal's connection since threadView will always be deleted before + // the ReplyThreadPopup + std::ignore = + this->ui_.threadView->mouseDown.connect([this](QMouseEvent *) { + this->giveFocus(Qt::MouseFocusReason); + }); // Create SplitInput with inline replying disabled this->ui_.replyInput = @@ -97,8 +100,10 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, this->updateInputUI(); })); - // clear SplitInput selection when selecting in ChannelView - this->ui_.threadView->selectionChanged.connect([this]() { + // We can safely ignore this signal's connection since threadView will always be deleted before + // the ReplyThreadPopup + std::ignore = this->ui_.threadView->selectionChanged.connect([this]() { + // clear SplitInput selection when selecting in ChannelView if (this->ui_.replyInput->hasSelection()) { this->ui_.replyInput->clearSelection(); @@ -106,7 +111,9 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, }); // clear ChannelView selection when selecting in SplitInput - this->ui_.replyInput->selectionChanged.connect([this]() { + // We can safely ignore this signal's connection since replyInput will always be deleted before + // the ReplyThreadPopup + std::ignore = this->ui_.replyInput->selectionChanged.connect([this]() { if (this->ui_.threadView->hasSelection()) { this->ui_.threadView->clearSelection(); diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index 1b4cb8965..4c1dba085 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -170,7 +170,9 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) view->getTableView()->horizontalHeader()->setSectionHidden(4, true); view->getTableView()->horizontalHeader()->setSectionHidden(5, true); - view->addButtonPressed.connect([] { + // We can safely ignore this signal's connection since the button won't be + // accessible after this dialog is closed + std::ignore = view->addButtonPressed.connect([] { auto unique = IrcServerData{}; unique.id = Irc::instance().uniqueId(); diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 481c35dac..701f427cd 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -438,8 +438,10 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, }); // userstate - this->userStateChanged_.connect([this, mod, unmod, vip, - unvip]() mutable { + // We can safely ignore this signal connection since this is a private signal, and + // we only connect once + std::ignore = this->userStateChanged_.connect([this, mod, unmod, vip, + unvip]() mutable { TwitchChannel *twitchChannel = dynamic_cast(this->underlyingChannel_.get()); @@ -469,17 +471,22 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, { auto timeout = moderation.emplace(); - this->userStateChanged_.connect([this, lineMod, timeout]() mutable { - TwitchChannel *twitchChannel = - dynamic_cast(this->underlyingChannel_.get()); + // We can safely ignore this signal connection since this is a private signal, and + // we only connect once + std::ignore = + this->userStateChanged_.connect([this, lineMod, timeout]() mutable { + TwitchChannel *twitchChannel = dynamic_cast( + this->underlyingChannel_.get()); - bool hasModRights = - twitchChannel ? twitchChannel->hasModRights() : false; - lineMod->setVisible(hasModRights); - timeout->setVisible(hasModRights); - }); + bool hasModRights = + twitchChannel ? twitchChannel->hasModRights() : false; + lineMod->setVisible(hasModRights); + timeout->setVisible(hasModRights); + }); - timeout->buttonClicked.connect([this](auto item) { + // We can safely ignore this signal connection since we own the button, and + // the button will always be destroyed before the UserInfoPopup + std::ignore = timeout->buttonClicked.connect([this](auto item) { TimeoutWidget::Action action; int arg; std::tie(action, arg) = item; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c8abae676..1ade8e9cb 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -227,7 +227,9 @@ void ChannelView::initializeLayout() void ChannelView::initializeScrollbar() { - this->scrollBar_->getCurrentValueChanged().connect([this] { + // We can safely ignore the scroll bar's signal connection since the scroll bar will + // always be destroyed before the ChannelView + std::ignore = this->scrollBar_->getCurrentValueChanged().connect([this] { if (this->isVisible()) { this->performLayout(true); diff --git a/src/widgets/settingspages/AccountsPage.cpp b/src/widgets/settingspages/AccountsPage.cpp index 9b2abb8a7..77b9c696d 100644 --- a/src/widgets/settingspages/AccountsPage.cpp +++ b/src/widgets/settingspages/AccountsPage.cpp @@ -33,7 +33,8 @@ AccountsPage::AccountsPage() view->getTableView()->horizontalHeader()->setVisible(false); view->getTableView()->horizontalHeader()->setStretchLastSection(true); - view->addButtonPressed.connect([this] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([this] { LoginDialog d(this); d.exec(); }); diff --git a/src/widgets/settingspages/CommandPage.cpp b/src/widgets/settingspages/CommandPage.cpp index 1e648f472..fbe7cd677 100644 --- a/src/widgets/settingspages/CommandPage.cpp +++ b/src/widgets/settingspages/CommandPage.cpp @@ -45,7 +45,8 @@ CommandPage::CommandPage() view->setTitles({"Trigger", "Command", "Show In\nMessage Menu"}); view->getTableView()->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getApp()->commands->items.append( Command{"/command", "I made a new command HeyGuys"}); }); diff --git a/src/widgets/settingspages/FiltersPage.cpp b/src/widgets/settingspages/FiltersPage.cpp index e28c7c5a7..d367a3e32 100644 --- a/src/widgets/settingspages/FiltersPage.cpp +++ b/src/widgets/settingspages/FiltersPage.cpp @@ -44,7 +44,8 @@ FiltersPage::FiltersPage() view->getTableView()->setColumnWidth(2, 125); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { ChannelFilterEditorDialog d( static_cast(&(getApp()->windows->getMainWindow()))); if (d.exec() == QDialog::Accepted) diff --git a/src/widgets/settingspages/GeneralPageView.cpp b/src/widgets/settingspages/GeneralPageView.cpp index 0d1ab0b25..b063c6177 100644 --- a/src/widgets/settingspages/GeneralPageView.cpp +++ b/src/widgets/settingspages/GeneralPageView.cpp @@ -195,13 +195,17 @@ ColorButton *GeneralPageView::addColorButton( auto dialog = new ColorPickerDialog(QColor(setting), this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); - dialog->closed.connect([&setting, colorButton](QColor selected) { - if (selected.isValid()) - { - setting = selected.name(QColor::HexArgb); - colorButton->setColor(selected); - } - }); + // We can safely ignore this signal connection, for now, since the + // colorButton & setting are never deleted and the signal is deleted + // once the dialog is closed + std::ignore = dialog->closed.connect( + [&setting, colorButton](QColor selected) { + if (selected.isValid()) + { + setting = selected.name(QColor::HexArgb); + colorButton->setColor(selected); + } + }); }); this->groups_.back().widgets.push_back({label, {text}}); diff --git a/src/widgets/settingspages/HighlightingPage.cpp b/src/widgets/settingspages/HighlightingPage.cpp index 60873bbcb..1b6e61f73 100644 --- a/src/widgets/settingspages/HighlightingPage.cpp +++ b/src/widgets/settingspages/HighlightingPage.cpp @@ -90,7 +90,8 @@ HighlightingPage::HighlightingPage() view->getTableView()->setColumnWidth(0, 400); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->highlightedMessages.append(HighlightPhrase{ "my phrase", true, true, false, false, false, "", *ColorProvider::instance().color( @@ -141,7 +142,8 @@ HighlightingPage::HighlightingPage() view->getTableView()->setColumnWidth(0, 200); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->highlightedUsers.append(HighlightPhrase{ "highlighted user", true, true, false, false, false, "", *ColorProvider::instance().color( @@ -182,7 +184,8 @@ HighlightingPage::HighlightingPage() view->getTableView()->setColumnWidth(0, 200); }); - view->addButtonPressed.connect([this] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([this] { auto d = std::make_shared( availableBadges, this); @@ -236,7 +239,8 @@ HighlightingPage::HighlightingPage() view->getTableView()->setColumnWidth(0, 200); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->blacklistedUsers.append( HighlightBlacklistUser{"blacklisted user", false}); }); @@ -329,7 +333,10 @@ void HighlightingPage::openColorDialog(const QModelIndex &clicked, auto dialog = new ColorPickerDialog(initial, this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); - dialog->closed.connect([=](auto selected) { + // We can safely ignore this signal connection since the view and tab are never deleted + // TODO: The QModelIndex clicked is technically not safe to persist here since the model + // can be changed between the color dialog being created & the color dialog being closed + std::ignore = dialog->closed.connect([=](auto selected) { if (selected.isValid()) { view->getModel()->setData(clicked, selected, Qt::DecorationRole); diff --git a/src/widgets/settingspages/IgnoresPage.cpp b/src/widgets/settingspages/IgnoresPage.cpp index d537265fe..9997fbcf0 100644 --- a/src/widgets/settingspages/IgnoresPage.cpp +++ b/src/widgets/settingspages/IgnoresPage.cpp @@ -63,7 +63,8 @@ void addPhrasesTab(LayoutCreator layout) view->getTableView()->setColumnWidth(0, 200); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->ignoredMessages.append( IgnorePhrase{"my pattern", false, false, getSettings()->ignoredPhraseReplace.getValue(), true}); diff --git a/src/widgets/settingspages/KeyboardSettingsPage.cpp b/src/widgets/settingspages/KeyboardSettingsPage.cpp index 011be06ac..61c4e668e 100644 --- a/src/widgets/settingspages/KeyboardSettingsPage.cpp +++ b/src/widgets/settingspages/KeyboardSettingsPage.cpp @@ -34,7 +34,8 @@ KeyboardSettingsPage::KeyboardSettingsPage() view->getTableView()->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch); - view->addButtonPressed.connect([view, model] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([view, model] { EditHotkeyDialog dialog(nullptr); bool wasAccepted = dialog.exec() == 1; diff --git a/src/widgets/settingspages/ModerationPage.cpp b/src/widgets/settingspages/ModerationPage.cpp index 6dfd6f5f0..0e9c2aa7b 100644 --- a/src/widgets/settingspages/ModerationPage.cpp +++ b/src/widgets/settingspages/ModerationPage.cpp @@ -169,7 +169,8 @@ ModerationPage::ModerationPage() view->getTableView()->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->loggedChannels.append(ChannelLog("channel")); }); @@ -209,7 +210,8 @@ ModerationPage::ModerationPage() view->getTableView()->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->moderationActions.append( ModerationAction("/timeout {user.name} 300")); }); diff --git a/src/widgets/settingspages/NicknamesPage.cpp b/src/widgets/settingspages/NicknamesPage.cpp index a61042bf9..6fbfb7ea7 100644 --- a/src/widgets/settingspages/NicknamesPage.cpp +++ b/src/widgets/settingspages/NicknamesPage.cpp @@ -36,7 +36,8 @@ NicknamesPage::NicknamesPage() view->getTableView()->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getSettings()->nicknames.append( Nickname{"Username", "Nickname", false, false}); }); diff --git a/src/widgets/settingspages/NotificationPage.cpp b/src/widgets/settingspages/NotificationPage.cpp index 8f88cf15c..18fe38d9b 100644 --- a/src/widgets/settingspages/NotificationPage.cpp +++ b/src/widgets/settingspages/NotificationPage.cpp @@ -109,7 +109,8 @@ NotificationPage::NotificationPage() view->getTableView()->setColumnWidth(0, 200); }); - view->addButtonPressed.connect([] { + // We can safely ignore this signal connection since we own the view + std::ignore = view->addButtonPressed.connect([] { getApp() ->notifications->channelMap[Platform::Twitch] .append("channel"); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 1b036ca4d..dd99bb906 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -247,7 +247,8 @@ Split::Split(QWidget *parent) this->updateInputPlaceholder(); // clear SplitInput selection when selecting in ChannelView - this->view_->selectionChanged.connect([this]() { + // this connection can be ignored since the ChannelView is owned by this Split + std::ignore = this->view_->selectionChanged.connect([this]() { if (this->input_->hasSelection()) { this->input_->clearSelection(); @@ -255,55 +256,60 @@ Split::Split(QWidget *parent) }); // clear ChannelView selection when selecting in SplitInput - this->input_->selectionChanged.connect([this]() { + // this connection can be ignored since the SplitInput is owned by this Split + std::ignore = this->input_->selectionChanged.connect([this]() { if (this->view_->hasSelection()) { this->view_->clearSelection(); } }); - this->view_->openChannelIn.connect([this]( - QString twitchChannel, - FromTwitchLinkOpenChannelIn openIn) { - ChannelPtr channel = getApp()->twitch->getOrAddChannel(twitchChannel); - switch (openIn) - { - case FromTwitchLinkOpenChannelIn::Split: - this->openSplitRequested.invoke(channel); - break; - case FromTwitchLinkOpenChannelIn::Tab: - this->joinChannelInNewTab(channel); - break; - case FromTwitchLinkOpenChannelIn::BrowserPlayer: - this->openChannelInBrowserPlayer(channel); - break; - case FromTwitchLinkOpenChannelIn::Streamlink: - this->openChannelInStreamlink(twitchChannel); - break; - default: - qCWarning(chatterinoWidget) - << "Unhandled \"FromTwitchLinkOpenChannelIn\" enum value: " - << static_cast(openIn); - } - }); + // this connection can be ignored since the ChannelView is owned by this Split + std::ignore = this->view_->openChannelIn.connect( + [this](QString twitchChannel, FromTwitchLinkOpenChannelIn openIn) { + ChannelPtr channel = + getApp()->twitch->getOrAddChannel(twitchChannel); + switch (openIn) + { + case FromTwitchLinkOpenChannelIn::Split: + this->openSplitRequested.invoke(channel); + break; + case FromTwitchLinkOpenChannelIn::Tab: + this->joinChannelInNewTab(channel); + break; + case FromTwitchLinkOpenChannelIn::BrowserPlayer: + this->openChannelInBrowserPlayer(channel); + break; + case FromTwitchLinkOpenChannelIn::Streamlink: + this->openChannelInStreamlink(twitchChannel); + break; + default: + qCWarning(chatterinoWidget) + << "Unhandled \"FromTwitchLinkOpenChannelIn\" enum " + "value: " + << static_cast(openIn); + } + }); - this->input_->textChanged.connect([this](const QString &newText) { - if (getSettings()->showEmptyInput) - { - // We always show the input regardless of the text, so we can early out here - return; - } + // this connection can be ignored since the SplitInput is owned by this Split + std::ignore = + this->input_->textChanged.connect([this](const QString &newText) { + if (getSettings()->showEmptyInput) + { + // We always show the input regardless of the text, so we can early out here + return; + } - if (newText.isEmpty()) - { - this->input_->hide(); - } - else if (this->input_->isHidden()) - { - // Text updated and the input was previously hidden, show it - this->input_->show(); - } - }); + if (newText.isEmpty()) + { + this->input_->hide(); + } + else if (this->input_->isHidden()) + { + // Text updated and the input was previously hidden, show it + this->input_->show(); + } + }); getSettings()->showEmptyInput.connect( [this](const bool &showEmptyInput, auto) { @@ -367,7 +373,9 @@ Split::Split(QWidget *parent) // Forward textEdit's focusLost event this->focusLost.invoke(); }); - this->input_->ui_.textEdit->imagePasted.connect( + + // 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) { if (!getSettings()->imageUploaderEnabled) return; @@ -896,7 +904,9 @@ void Split::showChangeChannelPopup(const char *dialogTitle, bool empty, dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(dialogTitle); dialog->show(); - dialog->closed.connect([=, this] { + // We can safely ignore this signal connection since the dialog will be closed before + // this Split is closed + std::ignore = dialog->closed.connect([=, this] { if (dialog->hasSeletedChannel()) { this->setChannel(dialog->getSelectedChannel()); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 3f2fad551..4991f7a95 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -44,43 +44,45 @@ using namespace chatterino; // 5 minutes constexpr const uint64_t THUMBNAIL_MAX_AGE_MS = 5ULL * 60 * 1000; -auto formatRoomMode(TwitchChannel &channel) -> QString +auto formatRoomModeUnclean( + const SharedAccessGuard &modes) -> QString { QString text; + if (modes->r9k) { - auto modes = channel.accessRoomModes(); - - if (modes->r9k) + text += "r9k, "; + } + if (modes->slowMode > 0) + { + text += QString("slow(%1), ").arg(localizeNumbers(modes->slowMode)); + } + if (modes->emoteOnly) + { + text += "emote, "; + } + if (modes->submode) + { + text += "sub, "; + } + if (modes->followerOnly != -1) + { + if (modes->followerOnly != 0) { - text += "r9k, "; + text += QString("follow(%1m), ") + .arg(localizeNumbers(modes->followerOnly)); } - if (modes->slowMode > 0) + else { - text += QString("slow(%1), ").arg(localizeNumbers(modes->slowMode)); - } - if (modes->emoteOnly) - { - text += "emote, "; - } - if (modes->submode) - { - text += "sub, "; - } - if (modes->followerOnly != -1) - { - if (modes->followerOnly != 0) - { - text += QString("follow(%1m), ") - .arg(localizeNumbers(modes->followerOnly)); - } - else - { - text += QString("follow, "); - } + text += QString("follow, "); } } + return text; +} + +void cleanRoomModeText(QString &text, bool hasModRights) +{ if (text.length() > 2) { text = text.mid(0, text.size() - 2); @@ -97,12 +99,10 @@ auto formatRoomMode(TwitchChannel &channel) -> QString } } - if (text.isEmpty() && channel.hasModRights()) + if (text.isEmpty() && hasModRights) { - return "none"; + text = "none"; } - - return text; } auto formatTooltip(const TwitchChannel::StreamStatus &s, QString thumbnail) @@ -231,13 +231,16 @@ SplitHeader::SplitHeader(Split *split) this->handleChannelChanged(); this->updateModerationModeIcon(); - this->split_->focused.connect([this]() { + // The lifetime of these signals are tied to the lifetime of the Split. + // Since the SplitHeader is owned by the Split, they will always be destroyed + // at the same time. + std::ignore = this->split_->focused.connect([this]() { this->themeChangedEvent(); }); - this->split_->focusLost.connect([this]() { + std::ignore = this->split_->focusLost.connect([this]() { this->themeChangedEvent(); }); - this->split_->channelChanged.connect([this]() { + std::ignore = this->split_->channelChanged.connect([this]() { this->handleChannelChanged(); }); @@ -257,6 +260,8 @@ SplitHeader::SplitHeader(Split *split) void SplitHeader::initializeLayout() { + assert(this->layout() == nullptr); + auto *layout = makeLayout({ // space makeWidget([](auto w) { @@ -277,7 +282,6 @@ void SplitHeader::initializeLayout() this->modeButton_ = makeWidget([&](auto w) { w->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); w->hide(); - this->initializeModeSignals(*w); w->setMenu(this->createChatModeMenu()); }), // moderator @@ -573,43 +577,23 @@ std::unique_ptr SplitHeader::createChatModeMenu() { auto menu = std::make_unique(); - auto *setSub = new QAction("Subscriber only", this); - auto *setEmote = new QAction("Emote only", this); - auto *setSlow = new QAction("Slow", this); - auto *setR9k = new QAction("R9K", this); - auto *setFollowers = new QAction("Followers only", this); + this->modeActionSetSub = new QAction("Subscriber only", this); + this->modeActionSetEmote = new QAction("Emote only", this); + this->modeActionSetSlow = new QAction("Slow", this); + this->modeActionSetR9k = new QAction("R9K", this); + this->modeActionSetFollowers = new QAction("Followers only", this); - setFollowers->setCheckable(true); - setSub->setCheckable(true); - setEmote->setCheckable(true); - setSlow->setCheckable(true); - setR9k->setCheckable(true); + this->modeActionSetFollowers->setCheckable(true); + this->modeActionSetSub->setCheckable(true); + this->modeActionSetEmote->setCheckable(true); + this->modeActionSetSlow->setCheckable(true); + this->modeActionSetR9k->setCheckable(true); - menu->addAction(setEmote); - menu->addAction(setSub); - menu->addAction(setSlow); - menu->addAction(setR9k); - menu->addAction(setFollowers); - - this->managedConnections_.managedConnect( - this->modeUpdateRequested_, - [this, setSub, setEmote, setSlow, setR9k, setFollowers]() { - auto *twitchChannel = - dynamic_cast(this->split_->getChannel().get()); - if (twitchChannel == nullptr) - { - this->modeButton_->hide(); - return; - } - - auto roomModes = twitchChannel->accessRoomModes(); - - setR9k->setChecked(roomModes->r9k); - setSlow->setChecked(roomModes->slowMode > 0); - setEmote->setChecked(roomModes->emoteOnly); - setSub->setChecked(roomModes->submode); - setFollowers->setChecked(roomModes->followerOnly != -1); - }); + menu->addAction(this->modeActionSetEmote); + menu->addAction(this->modeActionSetSub); + menu->addAction(this->modeActionSetSlow); + menu->addAction(this->modeActionSetR9k); + menu->addAction(this->modeActionSetFollowers); auto execCommand = [this](const QString &command) { auto text = getApp()->getCommands()->execCommand( @@ -622,44 +606,44 @@ std::unique_ptr SplitHeader::createChatModeMenu() action->setChecked(!action->isChecked()); }; - QObject::connect(setSub, &QAction::triggered, this, - [setSub, toggle]() mutable { - toggle("/subscribers", setSub); + QObject::connect(this->modeActionSetSub, &QAction::triggered, this, + [this, toggle]() mutable { + toggle("/subscribers", this->modeActionSetSub); }); - QObject::connect(setEmote, &QAction::triggered, this, - [setEmote, toggle]() mutable { - toggle("/emoteonly", setEmote); + QObject::connect(this->modeActionSetEmote, &QAction::triggered, this, + [this, toggle]() mutable { + toggle("/emoteonly", this->modeActionSetEmote); }); - QObject::connect( - setSlow, &QAction::triggered, this, [setSlow, this, execCommand]() { - if (!setSlow->isChecked()) - { - execCommand("/slowoff"); - setSlow->setChecked(false); - return; - }; - auto ok = bool(); - auto seconds = - QInputDialog::getInt(this, "", "Seconds:", 10, 0, 500, 1, &ok, - Qt::FramelessWindowHint); - if (ok) - { - execCommand(QString("/slow %1").arg(seconds)); - } - else - { - setSlow->setChecked(false); - } - }); + QObject::connect(this->modeActionSetSlow, &QAction::triggered, this, + [this, execCommand]() { + if (!this->modeActionSetSlow->isChecked()) + { + execCommand("/slowoff"); + this->modeActionSetSlow->setChecked(false); + return; + }; + auto ok = bool(); + auto seconds = QInputDialog::getInt( + this, "", "Seconds:", 10, 0, 500, 1, &ok, + Qt::FramelessWindowHint); + if (ok) + { + execCommand(QString("/slow %1").arg(seconds)); + } + else + { + this->modeActionSetSlow->setChecked(false); + } + }); - QObject::connect(setFollowers, &QAction::triggered, this, - [setFollowers, this, execCommand]() { - if (!setFollowers->isChecked()) + QObject::connect(this->modeActionSetFollowers, &QAction::triggered, this, + [this, execCommand]() { + if (!this->modeActionSetFollowers->isChecked()) { execCommand("/followersoff"); - setFollowers->setChecked(false); + this->modeActionSetFollowers->setChecked(false); return; }; auto ok = bool(); @@ -673,13 +657,13 @@ std::unique_ptr SplitHeader::createChatModeMenu() } else { - setFollowers->setChecked(false); + this->modeActionSetFollowers->setChecked(false); } }); - QObject::connect(setR9k, &QAction::triggered, this, - [setR9k, toggle]() mutable { - toggle("/r9kbeta", setR9k); + QObject::connect(this->modeActionSetR9k, &QAction::triggered, this, + [this, toggle]() mutable { + toggle("/r9kbeta", this->modeActionSetR9k); }); return menu; @@ -687,30 +671,47 @@ std::unique_ptr SplitHeader::createChatModeMenu() void SplitHeader::updateRoomModes() { - this->modeUpdateRequested_.invoke(); -} + assert(this->modeButton_ != nullptr); -void SplitHeader::initializeModeSignals(EffectLabel &label) -{ - this->modeUpdateRequested_.connect([this, &label] { - if (auto *twitchChannel = - dynamic_cast(this->split_->getChannel().get())) + // Update the mode button + if (auto *twitchChannel = + dynamic_cast(this->split_->getChannel().get())) + { + this->modeButton_->setEnable(twitchChannel->hasModRights()); + + QString text; { - label.setEnable(twitchChannel->hasModRights()); + auto roomModes = twitchChannel->accessRoomModes(); + text = formatRoomModeUnclean(roomModes); - // set the label text - auto text = formatRoomMode(*twitchChannel); + // Set menu action + this->modeActionSetR9k->setChecked(roomModes->r9k); + this->modeActionSetSlow->setChecked(roomModes->slowMode > 0); + this->modeActionSetEmote->setChecked(roomModes->emoteOnly); + this->modeActionSetSub->setChecked(roomModes->submode); + this->modeActionSetFollowers->setChecked(roomModes->followerOnly != + -1); + } + cleanRoomModeText(text, twitchChannel->hasModRights()); - if (!text.isEmpty()) - { - label.getLabel().setText(text); - label.show(); - return; - } + // set the label text + + if (!text.isEmpty()) + { + this->modeButton_->getLabel().setText(text); + this->modeButton_->show(); + } + else + { + this->modeButton_->hide(); } - label.hide(); - }); + // Update the mode button menu actions + } + else + { + this->modeButton_->hide(); + } } void SplitHeader::resetThumbnail() diff --git a/src/widgets/splits/SplitHeader.hpp b/src/widgets/splits/SplitHeader.hpp index 7e7d9e8c8..12e8c20a4 100644 --- a/src/widgets/splits/SplitHeader.hpp +++ b/src/widgets/splits/SplitHeader.hpp @@ -32,6 +32,8 @@ public: void updateChannelText(); void updateModerationModeIcon(); + // Invoked when SplitHeader should update anything refering to a TwitchChannel's mode + // has changed (e.g. sub mode toggled) void updateRoomModes(); protected: @@ -75,7 +77,14 @@ private: // ui Button *dropdownButton_{}; Label *titleLabel_{}; + EffectLabel *modeButton_{}; + QAction *modeActionSetEmote{}; + QAction *modeActionSetSub{}; + QAction *modeActionSetSlow{}; + QAction *modeActionSetR9k{}; + QAction *modeActionSetFollowers{}; + Button *moderationButton_{}; Button *viewersButton_{}; Button *addButton_{}; @@ -86,8 +95,8 @@ private: bool doubleClicked_{false}; bool menuVisible_{false}; - // signals - pajlada::Signals::NoArgSignal modeUpdateRequested_; + // managedConnections_ contains connections for signals that are not managed by us + // and don't change when the parent Split changes its underlying channel pajlada::Signals::SignalHolder managedConnections_; pajlada::Signals::SignalHolder channelConnections_; std::vector bSignals_; diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index 2c6dee623..1d9553dfc 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -63,7 +63,9 @@ SplitInput::SplitInput(QWidget *parent, Split *_chatWidget, // misc this->installKeyPressedEvent(); this->addShortcuts(); - this->ui_.textEdit->focusLost.connect([this] { + // The textEdit's signal will be destroyed before this SplitInput is + // destroyed, so we can safely ignore this signal's connection. + std::ignore = this->ui_.textEdit->focusLost.connect([this] { this->hideCompletionPopup(); }); this->scaleChangedEvent(this->scale()); @@ -293,23 +295,25 @@ void SplitInput::openEmotePopup() this->emotePopup_ = new EmotePopup(this); this->emotePopup_->setAttribute(Qt::WA_DeleteOnClose); - this->emotePopup_->linkClicked.connect([this](const Link &link) { - if (link.type == Link::InsertText) - { - QTextCursor cursor = this->ui_.textEdit->textCursor(); - QString textToInsert(link.value + " "); - - // If symbol before cursor isn't space or empty - // Then insert space before emote. - if (cursor.position() > 0 && - !this->getInputText()[cursor.position() - 1].isSpace()) + // The EmotePopup is closed & destroyed when this is destroyed, meaning it's safe to ignore this connection + std::ignore = + this->emotePopup_->linkClicked.connect([this](const Link &link) { + if (link.type == Link::InsertText) { - textToInsert = " " + textToInsert; + QTextCursor cursor = this->ui_.textEdit->textCursor(); + QString textToInsert(link.value + " "); + + // If symbol before cursor isn't space or empty + // Then insert space before emote. + if (cursor.position() > 0 && + !this->getInputText()[cursor.position() - 1].isSpace()) + { + textToInsert = " " + textToInsert; + } + this->insertText(textToInsert); + this->ui_.textEdit->activateWindow(); } - this->insertText(textToInsert); - this->ui_.textEdit->activateWindow(); - } - }); + }); } this->emotePopup_->resize(int(300 * this->emotePopup_->scale()), @@ -649,33 +653,40 @@ bool SplitInput::eventFilter(QObject *obj, QEvent *event) void SplitInput::installKeyPressedEvent() { - this->ui_.textEdit->keyPressed.disconnectAll(); - this->ui_.textEdit->keyPressed.connect([this](QKeyEvent *event) { - if (auto *popup = this->inputCompletionPopup_.data()) - { - if (popup->isVisible()) + // We can safely ignore this signal's connection because SplitInput owns + // the textEdit object, so it will always be deleted before SplitInput + std::ignore = + this->ui_.textEdit->keyPressed.connect([this](QKeyEvent *event) { + if (auto *popup = this->inputCompletionPopup_.data()) { - if (popup->eventFilter(nullptr, event)) + if (popup->isVisible()) { - event->accept(); - return; + if (popup->eventFilter(nullptr, event)) + { + event->accept(); + return; + } } } - } - // One of the last remaining of it's kind, the copy shortcut. - // For some bizarre reason Qt doesn't want this key be rebound. - // TODO(Mm2PL): Revisit in Qt6, maybe something changed? - if ((event->key() == Qt::Key_C || event->key() == Qt::Key_Insert) && - event->modifiers() == Qt::ControlModifier) - { - if (this->channelView_->hasSelection()) + // One of the last remaining of it's kind, the copy shortcut. + // For some bizarre reason Qt doesn't want this key be rebound. + // TODO(Mm2PL): Revisit in Qt6, maybe something changed? + if ((event->key() == Qt::Key_C || event->key() == Qt::Key_Insert) && + event->modifiers() == Qt::ControlModifier) { - this->channelView_->copySelectedText(); - event->accept(); + if (this->channelView_->hasSelection()) + { + this->channelView_->copySelectedText(); + event->accept(); + } } - } - }); + }); + +#ifdef DEBUG + assert(this->keyPressedEventInstalled == false); + this->keyPressedEventInstalled = true; +#endif } void SplitInput::mousePressEvent(QMouseEvent *event) diff --git a/src/widgets/splits/SplitInput.hpp b/src/widgets/splits/SplitInput.hpp index fe9693b02..eec7fab87 100644 --- a/src/widgets/splits/SplitInput.hpp +++ b/src/widgets/splits/SplitInput.hpp @@ -91,6 +91,9 @@ protected: void addShortcuts() override; void initLayout(); bool eventFilter(QObject *obj, QEvent *event) override; +#ifdef DEBUG + bool keyPressedEventInstalled{}; +#endif void installKeyPressedEvent(); void onCursorPositionChanged(); void onTextChanged();