From 3bf111a0914b5afc9bcacfa3d4a762be763ee88a Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2017 14:16:13 +0200 Subject: [PATCH] More progress on tab-complete There are missing parts to the "account-based" emotes that needs to be completed before emote completion can be considered done. For now, when I've been testing, I've been manually injecting the oauthClient and oauthToken to the settings file with the `user_subscriptions` scope --- src/accountmanager.cpp | 5 +++ src/application.cpp | 3 +- src/completionmanager.cpp | 52 +++++++++++++++++++++- src/completionmanager.hpp | 32 ++++++-------- src/emotemanager.cpp | 78 ++++++++++++++++++++++++++++++++- src/emotemanager.hpp | 27 ++++++++++-- src/ircmanager.cpp | 31 +------------ src/ircmanager.hpp | 2 - src/widgets/chatwidget.cpp | 1 + src/widgets/chatwidget.hpp | 5 ++- src/widgets/chatwidgetinput.cpp | 5 ++- src/widgets/mainwindow.cpp | 4 +- src/widgets/mainwindow.hpp | 7 ++- src/widgets/notebook.cpp | 4 +- src/widgets/notebook.hpp | 10 +++-- src/widgets/notebookpage.cpp | 1 + src/widgets/notebookpage.hpp | 2 + src/windowmanager.cpp | 7 ++- src/windowmanager.hpp | 5 ++- 19 files changed, 212 insertions(+), 69 deletions(-) diff --git a/src/accountmanager.cpp b/src/accountmanager.cpp index 4dcfdd67..c9683142 100644 --- a/src/accountmanager.cpp +++ b/src/accountmanager.cpp @@ -1,5 +1,7 @@ #include "accountmanager.hpp" +#include + namespace chatterino { namespace { @@ -25,6 +27,9 @@ AccountManager::AccountManager() if (!envUsername.isEmpty() && !envOauthToken.isEmpty()) { this->addTwitchUser(twitch::TwitchUser(envUsername, envOauthToken, "")); } + + pajlada::Settings::Setting::set( + "/accounts/current/roomID", "11148817", pajlada::Settings::SettingOption::DoNotWriteToJSON); } twitch::TwitchUser &AccountManager::getTwitchAnon() diff --git a/src/application.cpp b/src/application.cpp index 7d4d8bce..254a006d 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -9,7 +9,8 @@ namespace chatterino { // It will create the instances of the major classes, and connect their signals to each other Application::Application() - : windowManager(this->channelManager, this->colorScheme) + : completionManager(this->emoteManager) + , windowManager(this->channelManager, this->colorScheme, this->completionManager) , colorScheme(this->windowManager) , emoteManager(this->windowManager, this->resources) , resources(this->emoteManager, this->windowManager) diff --git a/src/completionmanager.cpp b/src/completionmanager.cpp index ff33f202..e040adad 100644 --- a/src/completionmanager.cpp +++ b/src/completionmanager.cpp @@ -1,8 +1,11 @@ #include "completionmanager.hpp" +#include "common.hpp" +#include "emotemanager.hpp" namespace chatterino { -CompletionManager::CompletionManager() +CompletionManager::CompletionManager(EmoteManager &_emoteManager) + : emoteManager(_emoteManager) { } @@ -10,11 +13,58 @@ CompletionModel *CompletionManager::createModel(const std::string &channelName) { CompletionModel *ret = new CompletionModel(); + this->updateModel(ret, channelName); + + this->emoteManager.bttvGlobalEmoteCodes.updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.ffzGlobalEmoteCodes.updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.bttvChannelEmoteCodes[channelName].updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.ffzChannelEmoteCodes[channelName].updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + return ret; } void CompletionManager::updateModel(CompletionModel *model, const std::string &channelName) { + model->emotes.clear(); + + for (const auto &m : this->emoteManager.twitchAccountEmotes) { + for (const auto &emoteName : m.second.emoteCodes) { + model->emotes.push_back(qS(emoteName)); + } + } + + std::vector &bttvGlobalEmoteCodes = this->emoteManager.bttvGlobalEmoteCodes; + for (const auto &m : bttvGlobalEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &ffzGlobalEmoteCodes = this->emoteManager.ffzGlobalEmoteCodes; + for (const auto &m : ffzGlobalEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &bttvChannelEmoteCodes = + this->emoteManager.bttvChannelEmoteCodes[channelName]; + for (const auto &m : bttvChannelEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &ffzChannelEmoteCodes = + this->emoteManager.ffzChannelEmoteCodes[channelName]; + for (const auto &m : ffzChannelEmoteCodes) { + model->emotes.push_back(qS(m)); + } } } // namespace chatterino diff --git a/src/completionmanager.hpp b/src/completionmanager.hpp index 191a4c67..29980ac0 100644 --- a/src/completionmanager.hpp +++ b/src/completionmanager.hpp @@ -1,12 +1,15 @@ #pragma once -#include +#include +#include #include namespace chatterino { -class CompletionModel : public QAbstractItemModel +class EmoteManager; + +class CompletionModel : public QAbstractListModel { public: virtual int columnCount(const QModelIndex & /*parent*/) const override @@ -16,34 +19,27 @@ public: virtual QVariant data(const QModelIndex &index, int role) const override { - // TODO: Implement - return QVariant(); - } - - virtual QModelIndex index(int row, int column, const QModelIndex &parent) const override - { - // TODO: Implement - return QModelIndex(); - } - - virtual QModelIndex parent(const QModelIndex &child) const override - { - return QModelIndex(); + // TODO: Implement more safely + return QVariant(this->emotes.at(index.row())); } virtual int rowCount(const QModelIndex &parent) const override { - return 1; + return this->emotes.size(); } + + QVector emotes; }; class CompletionManager { - CompletionManager(); + CompletionManager(EmoteManager &_emoteManager); + + EmoteManager &emoteManager; public: CompletionModel *createModel(const std::string &channelName); - void updateModel(CompletionModel *model, const std::string &channelName); + void updateModel(CompletionModel *model, const std::string &channelName = std::string()); friend class Application; }; diff --git a/src/emotemanager.cpp b/src/emotemanager.cpp index 438f00ae..eded4ed5 100644 --- a/src/emotemanager.cpp +++ b/src/emotemanager.cpp @@ -1,4 +1,5 @@ #include "emotemanager.hpp" +#include "common.hpp" #include "resources.hpp" #include "util/urlfetch.hpp" #include "windowmanager.hpp" @@ -25,6 +26,12 @@ EmoteManager::EmoteManager(WindowManager &_windowManager, Resources &_resources) , resources(_resources) { // Note: Do not use this->resources in ctor + pajlada::Settings::Setting roomID( + "/accounts/current/roomID", "", pajlada::Settings::SettingOption::DoNotWriteToJSON); + + roomID.getValueChangedSignal().connect([this](const std::string &roomID) { + this->refreshTwitchEmotes(roomID); // + }); } void EmoteManager::loadGlobalEmotes() @@ -48,6 +55,7 @@ void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName) QString linkTemplate = "https:" + rootNode.value("urlTemplate").toString(); + std::vector codes; for (const QJsonValue &emoteNode : emotesNode) { QJsonObject emoteObject = emoteNode.toObject(); @@ -67,7 +75,10 @@ void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName) this->bttvChannelEmotes.insert(code, emote); channelEmoteMap.insert(code, emote); + codes.push_back(code.toStdString()); } + + this->bttvChannelEmoteCodes[channelName.toStdString()] = codes; }); } @@ -84,6 +95,7 @@ void EmoteManager::reloadFFZChannelEmotes(const QString &channelName) auto setsNode = rootNode.value("sets").toObject(); + std::vector codes; for (const QJsonValue &setNode : setsNode) { auto emotesNode = setNode.toObject().value("emoticons").toArray(); @@ -105,7 +117,10 @@ void EmoteManager::reloadFFZChannelEmotes(const QString &channelName) this->ffzChannelEmotes.insert(code, emote); channelEmoteMap.insert(code, emote); + codes.push_back(code.toStdString()); } + + this->ffzChannelEmoteCodes[channelName.toStdString()] = codes; } }); } @@ -279,9 +294,61 @@ void EmoteManager::parseEmojis(std::vector> &pars } } +void EmoteManager::refreshTwitchEmotes(const std::string &roomID) +{ + std::string oauthClient = + pajlada::Settings::Setting::get("/accounts/" + roomID + "/oauthClient"); + std::string oauthToken = + pajlada::Settings::Setting::get("/accounts/" + roomID + "/oauthToken"); + + TwitchAccountEmoteData &emoteData = this->twitchAccountEmotes[roomID]; + + if (emoteData.filled) { + qDebug() << "Already loaded for room id " << qS(roomID); + return; + } + + qDebug() << "Loading emotes for room id " << qS(roomID); + + if (oauthClient.empty() || oauthToken.empty()) { + qDebug() << "Missing oauth client/token"; + return; + } + + // api:v5 + QString url("https://api.twitch.tv/kraken/users/" + qS(roomID) + + "/emotes?api_version=5&oauth_token=" + qS(oauthToken) + + "&client_id=" + qS(oauthClient)); + + qDebug() << url; + + util::urlFetchJSONTimeout( + url, + [=, &emoteData](QJsonObject &root) { + emoteData.emoteSets.clear(); + emoteData.emoteCodes.clear(); + + auto emoticonSets = root.value("emoticon_sets").toObject(); + for (QJsonObject::iterator it = emoticonSets.begin(); it != emoticonSets.end(); ++it) { + std::string emoteSetString = it.key().toStdString(); + QJsonArray emoteSetList = it.value().toArray(); + + for (QJsonValue emoteValue : emoteSetList) { + QJsonObject emoticon = emoteValue.toObject(); + std::string id = emoticon["id"].toString().toStdString(); + std::string code = emoticon["code"].toString().toStdString(); + emoteData.emoteSets[emoteSetString].push_back({id, code}); + emoteData.emoteCodes.push_back(code); + } + } + + emoteData.filled = true; + }, + 3000); +} + void EmoteManager::loadBTTVEmotes() { - // bttv QNetworkAccessManager *manager = new QNetworkAccessManager(); QUrl url("https://api.betterttv.net/2/emotes"); @@ -299,6 +366,7 @@ void EmoteManager::loadBTTVEmotes() QString linkTemplate = "https:" + root.value("urlTemplate").toString(); + std::vector codes; for (const QJsonValue &emote : emotes) { QString id = emote.toObject().value("id").toString(); QString code = emote.toObject().value("code").toString(); @@ -311,7 +379,10 @@ void EmoteManager::loadBTTVEmotes() this->bttvGlobalEmotes.insert( code, new LazyLoadedImage(*this, this->windowManager, url, 1, code, code + "\nGlobal BTTV Emote")); + codes.push_back(code.toStdString()); } + + this->bttvGlobalEmoteCodes = codes; } reply->deleteLater(); @@ -321,7 +392,6 @@ void EmoteManager::loadBTTVEmotes() void EmoteManager::loadFFZEmotes() { - // ffz QNetworkAccessManager *manager = new QNetworkAccessManager(); QUrl url("https://api.frankerfacez.com/v1/set/global"); @@ -337,6 +407,7 @@ void EmoteManager::loadFFZEmotes() auto sets = root.value("sets").toObject(); + std::vector codes; for (const QJsonValue &set : sets) { auto emoticons = set.toObject().value("emoticons").toArray(); @@ -354,7 +425,10 @@ void EmoteManager::loadFFZEmotes() this->ffzGlobalEmotes.insert( code, new LazyLoadedImage(*this, this->windowManager, url1, 1, code, code + "\nGlobal FFZ Emote")); + codes.push_back(code.toStdString()); } + + this->ffzGlobalEmoteCodes = codes; } } diff --git a/src/emotemanager.hpp b/src/emotemanager.hpp index 1cce56a2..7324ce67 100644 --- a/src/emotemanager.hpp +++ b/src/emotemanager.hpp @@ -5,6 +5,7 @@ #include "concurrentmap.hpp" #include "emojis.hpp" #include "messages/lazyloadedimage.hpp" +#include "signalvector.hpp" #include "twitch/emotevalue.hpp" #include @@ -88,11 +89,26 @@ private: public: void parseEmojis(std::vector> &parsedWords, const QString &text); -private: /// Twitch emotes - // username emote code - ConcurrentStdMap> twitchAccountEmotes; + void refreshTwitchEmotes(const std::string &roomID); + struct TwitchAccountEmoteData { + struct TwitchEmote { + std::string id; + std::string code; + }; + + // emote set + std::map> emoteSets; + + std::vector emoteCodes; + + bool filled = false; + }; + + std::map twitchAccountEmotes; + +private: // emote code ConcurrentMap _twitchEmotes; @@ -105,6 +121,9 @@ private: public: ConcurrentMap bttvChannels; EmoteMap bttvGlobalEmotes; + SignalVector bttvGlobalEmoteCodes; + // roomID + std::map> bttvChannelEmoteCodes; EmoteMap _bttvChannelEmoteFromCaches; private: @@ -116,6 +135,8 @@ private: public: ConcurrentMap ffzChannels; EmoteMap ffzGlobalEmotes; + SignalVector ffzGlobalEmoteCodes; + std::map> ffzChannelEmoteCodes; private: ConcurrentMap _ffzChannelEmoteFromCaches; diff --git a/src/ircmanager.cpp b/src/ircmanager.cpp index 6dfbfef3..4aa3ba30 100644 --- a/src/ircmanager.cpp +++ b/src/ircmanager.cpp @@ -79,8 +79,6 @@ Communi::IrcConnection *IrcManager::createConnection(bool doRead) this->refreshIgnoredUsers(username, oauthClient, oauthToken); } - this->refreshTwitchEmotes(username, oauthClient, oauthToken); - if (doRead) { connection->sendCommand( Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership")); @@ -130,34 +128,6 @@ void IrcManager::refreshIgnoredUsers(const QString &username, const QString &oau }); } -void IrcManager::refreshTwitchEmotes(const QString &username, const QString &oauthClient, - const QString &oauthToken) -{ - QString url("https://api.twitch.tv/kraken/users/" + username + - "/emotes?oauth_token=" + oauthToken + "&client_id=" + oauthClient); - - if (true) { - util::urlFetchJSONTimeout( - url, - [=](QJsonObject &root) { - // nextLink = - // root.value("_links").toObject().value("next").toString(); - - auto blocks = root.value("blocks").toArray(); - - _twitchBlockedUsersMutex.lock(); - for (QJsonValue block : blocks) { - QJsonObject user = block.toObject().value("user").toObject(); - // display_name - _twitchBlockedUsers.insert(user.value("name").toString().toLower(), true); - } - _twitchBlockedUsersMutex.unlock(); - qDebug() << "XD"; - }, - 3000, &this->networkAccessManager); - } -} - void IrcManager::beginConnecting() { uint32_t generation = ++this->connectionGeneration; @@ -305,6 +275,7 @@ void IrcManager::handleRoomStateMessage(Communi::IrcMessage *message) if (iterator != tags.end()) { std::string roomID = iterator.value().toString().toStdString(); + this->resources.loadChannelData(roomID); } } diff --git a/src/ircmanager.hpp b/src/ircmanager.hpp index a1f597fa..b658ae41 100644 --- a/src/ircmanager.hpp +++ b/src/ircmanager.hpp @@ -76,8 +76,6 @@ private: void refreshIgnoredUsers(const QString &username, const QString &oauthClient, const QString &oauthToken); - void refreshTwitchEmotes(const QString &username, const QString &oauthClient, - const QString &oauthToken); void beginConnecting(); diff --git a/src/widgets/chatwidget.cpp b/src/widgets/chatwidget.cpp index 8c7b0672..92e2331a 100644 --- a/src/widgets/chatwidget.cpp +++ b/src/widgets/chatwidget.cpp @@ -37,6 +37,7 @@ static int index = 0; ChatWidget::ChatWidget(ChannelManager &_channelManager, NotebookPage *parent) : BaseWidget(parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , channel(_channelManager.emptyChannel) , vbox(this) , header(this) diff --git a/src/widgets/chatwidget.hpp b/src/widgets/chatwidget.hpp index 607cfda5..64a038ee 100644 --- a/src/widgets/chatwidget.hpp +++ b/src/widgets/chatwidget.hpp @@ -21,6 +21,7 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; namespace widgets { @@ -59,9 +60,11 @@ public: protected: virtual void paintEvent(QPaintEvent *) override; -private: +public: ChannelManager &channelManager; + CompletionManager &completionManager; +private: void setChannel(std::shared_ptr newChannel); void detachChannel(); diff --git a/src/widgets/chatwidgetinput.cpp b/src/widgets/chatwidgetinput.cpp index 9bc7a329..acddf1af 100644 --- a/src/widgets/chatwidgetinput.cpp +++ b/src/widgets/chatwidgetinput.cpp @@ -1,6 +1,7 @@ #include "widgets/chatwidgetinput.hpp" #include "chatwidget.hpp" #include "colorscheme.hpp" +#include "completionmanager.hpp" #include "ircmanager.hpp" #include "settingsmanager.hpp" @@ -45,8 +46,8 @@ ChatWidgetInput::ChatWidgetInput(ChatWidget *_chatWidget) this->refreshTheme(); this->setMessageLengthVisible(SettingsManager::getInstance().showMessageLength.get()); - // TODO: Fill in this QCompleter model using our CompletionManager - auto completer = new QCompleter(); + auto completer = new QCompleter( + this->chatWidget->completionManager.createModel(this->chatWidget->channelName)); this->textInput.setCompleter(completer); diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 3e96f3c7..99c5bce2 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -19,10 +19,12 @@ namespace chatterino { namespace widgets { -MainWindow::MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme) +MainWindow::MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager) : BaseWidget(_colorScheme, nullptr) , channelManager(_channelManager) , colorScheme(_colorScheme) + , completionManager(_completionManager) , notebook(this->channelManager, this) , windowGeometry("/windows/0/geometry") { diff --git a/src/widgets/mainwindow.hpp b/src/widgets/mainwindow.hpp index 2a722ab5..37decd0f 100644 --- a/src/widgets/mainwindow.hpp +++ b/src/widgets/mainwindow.hpp @@ -17,6 +17,7 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; namespace widgets { @@ -25,7 +26,8 @@ class MainWindow : public BaseWidget Q_OBJECT public: - explicit MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme); + explicit MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager); ~MainWindow(); void layoutVisibleChatWidgets(Channel *channel = nullptr); @@ -48,6 +50,7 @@ private: ChannelManager &channelManager; ColorScheme &colorScheme; + CompletionManager &completionManager; Notebook notebook; bool loaded = false; @@ -144,6 +147,8 @@ private: }; pajlada::Settings::Setting windowGeometry; + + friend class Notebook; }; } // namespace widgets diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index f9d2ba78..fa81ec10 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -1,5 +1,6 @@ #include "widgets/notebook.hpp" #include "colorscheme.hpp" +#include "widgets/mainwindow.hpp" #include "widgets/notebookbutton.hpp" #include "widgets/notebookpage.hpp" #include "widgets/notebooktab.hpp" @@ -18,9 +19,10 @@ namespace chatterino { namespace widgets { -Notebook::Notebook(ChannelManager &_channelManager, BaseWidget *parent) +Notebook::Notebook(ChannelManager &_channelManager, MainWindow *parent) : BaseWidget(parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , addButton(this) , settingsButton(this) , userButton(this) diff --git a/src/widgets/notebook.hpp b/src/widgets/notebook.hpp index d510584d..2b608533 100644 --- a/src/widgets/notebook.hpp +++ b/src/widgets/notebook.hpp @@ -12,10 +12,12 @@ namespace chatterino { class ChannelManager; -class ColorScheme; +class CompletionManager; namespace widgets { +class MainWindow; + class Notebook : public BaseWidget { Q_OBJECT @@ -23,7 +25,7 @@ class Notebook : public BaseWidget public: enum HighlightType { none, highlighted, newMessage }; - explicit Notebook(ChannelManager &_channelManager, BaseWidget *parent); + explicit Notebook(ChannelManager &_channelManager, MainWindow *parent); NotebookPage *addPage(bool select = false); @@ -50,9 +52,11 @@ public slots: void usersButtonClicked(); void addPageButtonClicked(); -private: +public: ChannelManager &channelManager; + CompletionManager &completionManager; +private: QList pages; NotebookButton addButton; diff --git a/src/widgets/notebookpage.cpp b/src/widgets/notebookpage.cpp index 7563487e..fbec4e61 100644 --- a/src/widgets/notebookpage.cpp +++ b/src/widgets/notebookpage.cpp @@ -25,6 +25,7 @@ std::pair NotebookPage::dropPosition = std::pair(-1, -1); NotebookPage::NotebookPage(ChannelManager &_channelManager, Notebook *parent, NotebookTab *_tab) : BaseWidget(parent->colorScheme, parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , tab(_tab) , dropPreview(this) { diff --git a/src/widgets/notebookpage.hpp b/src/widgets/notebookpage.hpp index 6f8410a3..65ad64c4 100644 --- a/src/widgets/notebookpage.hpp +++ b/src/widgets/notebookpage.hpp @@ -18,6 +18,7 @@ namespace chatterino { class ChannelManager; +class CompletionManager; namespace widgets { @@ -29,6 +30,7 @@ public: NotebookPage(ChannelManager &_channelManager, Notebook *parent, NotebookTab *_tab); ChannelManager &channelManager; + CompletionManager &completionManager; std::pair removeFromLayout(ChatWidget *widget); void addToLayout(ChatWidget *widget, std::pair position); diff --git a/src/windowmanager.cpp b/src/windowmanager.cpp index b3d7fd59..ac0dbc9f 100644 --- a/src/windowmanager.cpp +++ b/src/windowmanager.cpp @@ -10,9 +10,11 @@ namespace chatterino { -WindowManager::WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme) +WindowManager::WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager) : channelManager(_channelManager) , colorScheme(_colorScheme) + , completionManager(_completionManager) { } @@ -56,7 +58,8 @@ widgets::MainWindow &WindowManager::getMainWindow() std::lock_guard lock(this->windowMutex); if (this->mainWindow == nullptr) { - this->mainWindow = new widgets::MainWindow(this->channelManager, this->colorScheme); + this->mainWindow = new widgets::MainWindow(this->channelManager, this->colorScheme, + this->completionManager); } return *this->mainWindow; diff --git a/src/windowmanager.hpp b/src/windowmanager.hpp index 4cdb0552..cbf18ae2 100644 --- a/src/windowmanager.hpp +++ b/src/windowmanager.hpp @@ -8,14 +8,17 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; class WindowManager { public: - explicit WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme); + explicit WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager); ChannelManager &channelManager; ColorScheme &colorScheme; + CompletionManager &completionManager; void layoutVisibleChatWidgets(Channel *channel = nullptr); void repaintVisibleChatWidgets(Channel *channel = nullptr);