diff --git a/chatterino.pro b/chatterino.pro index 5af054bd8..b8417b51b 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -184,7 +184,9 @@ SOURCES += \ src/widgets/lastruncrashdialog.cpp \ src/widgets/attachedwindow.cpp \ src/widgets/settingspages/externaltoolspage.cpp \ - src/widgets/helper/comboboxitemdelegate.cpp + src/widgets/helper/comboboxitemdelegate.cpp \ + src/util/signalvectormodel.cpp \ + src/managers/commands/command.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -316,7 +318,9 @@ HEADERS += \ src/util/standarditemhelper.hpp \ src/widgets/helper/comboboxitemdelegate.hpp \ src/util/assertinguithread.hpp \ - src/util/signalvector2.hpp + src/util/signalvector2.hpp \ + src/util/signalvectormodel.hpp \ + src/managers/commands/command.hpp RESOURCES += \ resources/resources.qrc diff --git a/lib/settings b/lib/settings index 94edfacf1..ad31b3886 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 94edfacf14728faf3aa1d9c058e89395c97aae14 +Subproject commit ad31b38866d80a17ced902476ed06da69edce3a0 diff --git a/src/application.cpp b/src/application.cpp index fd6e155f5..d9632f68a 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -93,7 +93,7 @@ void Application::initialize() this->nativeMessaging->registerHost(); this->settings->load(); - this->commands->loadCommands(); + this->commands->load(); this->emotes->loadGlobalEmotes(); @@ -218,7 +218,7 @@ void Application::save() { this->windows->save(); - this->commands->saveCommands(); + this->commands->save(); } void Application::runNativeMessagingHost() diff --git a/src/managers/commands/command.cpp b/src/managers/commands/command.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/managers/commands/command.hpp b/src/managers/commands/command.hpp new file mode 100644 index 000000000..d4b67fd99 --- /dev/null +++ b/src/managers/commands/command.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace chatterino { +namespace managers { +namespace commands { +// code +} +} // namespace managers +} // namespace chatterino diff --git a/src/providers/twitch/twitchmessagebuilder.cpp b/src/providers/twitch/twitchmessagebuilder.cpp index d4843986e..bf1f26938 100644 --- a/src/providers/twitch/twitchmessagebuilder.cpp +++ b/src/providers/twitch/twitchmessagebuilder.cpp @@ -74,8 +74,7 @@ MessagePtr TwitchMessageBuilder::build() #ifdef XD if (this->originalMessage.length() > 100) { this->message->flags |= Message::Collapsed; - this->emplace(singletons::ResourceManager::getInstance().badgeCollapsed, - MessageElement::Collapsed); + this->emplace(getApp()->resources->badgeCollapsed, MessageElement::Collapsed); } #endif diff --git a/src/singletons/commandmanager.cpp b/src/singletons/commandmanager.cpp index d11877f19..29d82645f 100644 --- a/src/singletons/commandmanager.cpp +++ b/src/singletons/commandmanager.cpp @@ -19,7 +19,24 @@ using namespace chatterino::providers::twitch; namespace chatterino { namespace singletons { -void CommandManager::loadCommands() +CommandManager::CommandManager() +{ + auto addFirstMatchToMap = [this](auto args) { + this->commandsMap.remove(args.item.name); + + for (const Command &cmd : this->commands.getVector()) { + if (cmd.name == args.item.name) { + this->commandsMap[cmd.name] = cmd; + break; + } + } + }; + + this->commands.itemInserted.connect(addFirstMatchToMap); + this->commands.itemRemoved.connect(addFirstMatchToMap); +} + +void CommandManager::load() { auto app = getApp(); this->filePath = app->paths->customFolderPath + "/Commands.txt"; @@ -32,18 +49,14 @@ void CommandManager::loadCommands() QList test = textFile.readAll().split('\n'); - QStringList loadedCommands; - for (const auto &command : test) { - loadedCommands.append(command); + this->commands.appendItem(Command(command)); } - this->setCommands(loadedCommands); - textFile.close(); } -void CommandManager::saveCommands() +void CommandManager::save() { QFile textFile(this->filePath); if (!textFile.open(QIODevice::WriteOnly)) { @@ -51,44 +64,16 @@ void CommandManager::saveCommands() return; } - QString commandsString = this->commandsStringList.join('\n'); - - textFile.write(commandsString.toUtf8()); + for (const Command &cmd : this->commands.getVector()) { + textFile.write((cmd.toString() + "\n").toUtf8()); + } textFile.close(); } -void CommandManager::setCommands(const QStringList &_commands) +CommandModel *CommandManager::createModel(QObject *parent) { - std::lock_guard lock(this->mutex); - - this->commands.clear(); - - for (const QString &commandRef : _commands) { - QString command = commandRef; - - if (command.size() == 0) { - continue; - } - - // if (command.at(0) != '/') { - // command = QString("/") + command; - // } - - QString commandName = command.mid(0, command.indexOf(' ')); - - if (this->commands.find(commandName) == this->commands.end()) { - this->commands.insert(commandName, Command(command)); - } - } - - this->commandsStringList = _commands; - this->commandsStringList.detach(); -} - -QStringList CommandManager::getCommands() -{ - return this->commandsStringList; + return new CommandModel(&this->commands, parent); } QString CommandManager::execCommand(const QString &text, ChannelPtr channel, bool dryRun) @@ -173,9 +158,8 @@ QString CommandManager::execCommand(const QString &text, ChannelPtr channel, boo } // check if custom command exists - auto it = this->commands.find(commandName); - - if (it == this->commands.end()) { + auto it = this->commandsMap.find(commandName); + if (it == this->commandsMap.end()) { return text; } @@ -193,17 +177,17 @@ QString CommandManager::execCustomCommand(const QStringList &words, const Comman int lastCaptureEnd = 0; - auto globalMatch = parseCommand.globalMatch(command.text); + auto globalMatch = parseCommand.globalMatch(command.func); int matchOffset = 0; while (true) { - QRegularExpressionMatch match = parseCommand.match(command.text, matchOffset); + QRegularExpressionMatch match = parseCommand.match(command.func, matchOffset); if (!match.hasMatch()) { break; } - result += command.text.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1); + result += command.func.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1); lastCaptureEnd = match.capturedEnd(); matchOffset = lastCaptureEnd - 1; @@ -238,7 +222,7 @@ QString CommandManager::execCustomCommand(const QStringList &words, const Comman } } - result += command.text.mid(lastCaptureEnd); + result += command.func.mid(lastCaptureEnd); if (result.size() > 0 && result.at(0) == '{') { result = result.mid(1); @@ -247,7 +231,30 @@ QString CommandManager::execCustomCommand(const QStringList &words, const Comman return result.replace("{{", "{"); } -CommandManager::Command::Command(QString _text) +// commandmodel +CommandModel::CommandModel(util::BaseSignalVector *vec, QObject *parent) + : util::SignalVectorModel(vec, 2, parent) +{ +} + +int CommandModel::prepareInsert(const Command &item, int index, + std::vector &rowToAdd) +{ + rowToAdd[0]->setData(item.name, Qt::EditRole); + rowToAdd[1]->setData(item.func, Qt::EditRole); + + return index; +} + +int CommandModel::prepareRemove(const Command &item, int index) +{ + UNUSED(item); + + return index; +} + +// command +Command::Command(const QString &_text) { int index = _text.indexOf(' '); @@ -257,7 +264,18 @@ CommandManager::Command::Command(QString _text) } this->name = _text.mid(0, index); - this->text = _text.mid(index + 1); + this->func = _text.mid(index + 1); +} + +Command::Command(const QString &_name, const QString &_func) + : name(_name) + , func(_func) +{ +} + +QString Command::toString() const +{ + return this->name + " " + this->func; } } // namespace singletons diff --git a/src/singletons/commandmanager.hpp b/src/singletons/commandmanager.hpp index a139c5356..2026d483a 100644 --- a/src/singletons/commandmanager.hpp +++ b/src/singletons/commandmanager.hpp @@ -6,40 +6,60 @@ #include #include +#include +#include + namespace chatterino { class Channel; namespace singletons { +class CommandManager; + +struct Command { + QString name; + QString func; + + Command() = default; + explicit Command(const QString &text); + Command(const QString &name, const QString &func); + + QString toString() const; +}; + +class CommandModel : public util::SignalVectorModel +{ + explicit CommandModel(util::BaseSignalVector *vec, QObject *parent); + +protected: + virtual int prepareInsert(const Command &item, int index, + std::vector &rowToAdd) override; + virtual int prepareRemove(const Command &item, int index) override; + + friend class CommandManager; +}; + // // this class managed the custom /commands // - class CommandManager { public: - CommandManager() = default; + CommandManager(); QString execCommand(const QString &text, std::shared_ptr channel, bool dryRun); - void loadCommands(); - void saveCommands(); + void load(); + void save(); - void setCommands(const QStringList &commands); - QStringList getCommands(); + CommandModel *createModel(QObject *parent); + + util::UnsortedSignalVector commands; private: - struct Command { - QString name; - QString text; + QMap commandsMap; - Command() = default; - Command(QString text); - }; - - QMap commands; std::mutex mutex; - QStringList commandsStringList; QString filePath; QString execCustomCommand(const QStringList &words, const Command &command); diff --git a/src/util/assertinguithread.hpp b/src/util/assertinguithread.hpp index f9ce99fde..9ea79ba69 100644 --- a/src/util/assertinguithread.hpp +++ b/src/util/assertinguithread.hpp @@ -7,7 +7,7 @@ namespace chatterino { namespace util { -void assertInGuiThread() +static void assertInGuiThread() { #ifdef _DEBUG assert(QCoreApplication::instance()->thread() == QThread::currentThread()); diff --git a/src/util/signalvector2.hpp b/src/util/signalvector2.hpp index fd9069330..f0d9e7217 100644 --- a/src/util/signalvector2.hpp +++ b/src/util/signalvector2.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -10,18 +12,24 @@ namespace chatterino { namespace util { template -class ReadOnlySignalVector +class ReadOnlySignalVector : boost::noncopyable { public: + ReadOnlySignalVector() + { + QObject::connect(&this->itemsChangedTimer, &QTimer::timeout, + [this] { this->delayedItemsChanged.invoke(); }); + } virtual ~ReadOnlySignalVector() = default; - struct ItemInsertedArgs { + struct ItemArgs { const TVectorItem &item; int index; + void *caller; }; - pajlada::Signals::Signal itemInserted; - pajlada::Signals::Signal itemRemoved; + pajlada::Signals::Signal itemInserted; + pajlada::Signals::Signal itemRemoved; pajlada::Signals::NoArgSignal delayedItemsChanged; const std::vector &getVector() const @@ -31,42 +39,56 @@ public: return this->vector; } + void invokeDelayedItemsChanged() + { + util::assertInGuiThread(); + + if (!this->itemsChangedTimer.isActive()) { + itemsChangedTimer.start(); + } + } + protected: std::vector vector; + QTimer itemsChangedTimer; }; template class BaseSignalVector : public ReadOnlySignalVector { public: - void removeItem(int index) + virtual void appendItem(const TVectorItem &item, void *caller = 0) = 0; + + void removeItem(int index, void *caller = 0) { util::assertInGuiThread(); assert(index >= 0 && index < this->vector.size()); + TVectorItem item = this->vector[index]; this->vector.erase(this->vector.begin() + index); - this->itemRemoved.invoke(index); + ItemArgs args{item, args, caller}; + this->itemRemoved.invoke(args); } }; template -class SignalVector2 : public BaseSignalVector +class UnsortedSignalVector : public BaseSignalVector { public: - void insertItem(const TVectorItem &item, int index) + void insertItem(const TVectorItem &item, int index, void *caller = 0) { util::assertInGuiThread(); assert(index >= 0 && index <= this->vector.size()); this->vector.insert(this->vector.begin() + index, item); - ItemInsertedArgs args{item, index}; + ItemArgs args{item, index, caller}; this->itemInserted.invoke(args); } - void appendItem(const TVectorItem &item) + virtual void appendItem(const TVectorItem &item, void *caller = 0) override { - this->insertItem(item, this->vector.size()); + this->insertItem(item, this->vector.size(), caller); } }; @@ -74,14 +96,14 @@ template class SortedSignalVector : public BaseSignalVector { public: - void addItem(const TVectorItem &item) + virtual void appendItem(const TVectorItem &item, void *caller = 0) override { util::assertInGuiThread(); int index = this->vector.insert( std::lower_bound(this->vector.begin(), this->vector.end(), item), item) - this->vector.begin(); - ItemInsertedArgs args{item, index}; + ItemArgs args{item, index, caller}; this->itemInserted.invoke(args); } }; diff --git a/src/util/signalvectormodel.cpp b/src/util/signalvectormodel.cpp new file mode 100644 index 000000000..7b74a3639 --- /dev/null +++ b/src/util/signalvectormodel.cpp @@ -0,0 +1 @@ +#include "signalvectormodel.hpp" diff --git a/src/util/signalvectormodel.hpp b/src/util/signalvectormodel.hpp new file mode 100644 index 000000000..82870cbd6 --- /dev/null +++ b/src/util/signalvectormodel.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include + +#include + +namespace chatterino { +namespace util { + +template +class SignalVectorModel : public QAbstractTableModel, pajlada::Signals::SignalHolder +{ +public: + SignalVectorModel(util::BaseSignalVector *vec, int columnCount, + QObject *parent = nullptr) + : QAbstractTableModel(parent) + , _columnCount(columnCount) + { + this->managedConnect(vec->itemInserted, [this](auto args) { + std::vector items; + for (int i = 0; i < this->_columnCount; i++) { + items.push_back(new QStandardItem()); + } + + int row = this->prepareInsert(args.item, args.index, items); + assert(row >= 0 && row <= this->rows.size()); + + // insert row + this->beginInsertRows(QModelIndex(), row, row); + this->rows.insert(this->rows.begin() + row, Row(items)); + this->endInsertRows(); + }); + this->managedConnect(vec->itemRemoved, [this](auto args) { + int row = this->prepareRemove(args.item, args.index); + assert(row >= 0 && row <= this->rows.size()); + + // remove row + this->beginRemoveRows(QModelIndex(), row, row); + for (QStandardItem *item : this->rows[row].items) { + delete item; + } + this->rows.erase(this->rows.begin() + row); + this->endRemoveRows(); + }); + } + + virtual ~SignalVectorModel() + { + for (Row &row : this->rows) { + for (QStandardItem *item : row.items) { + delete item; + } + } + } + + int rowCount(const QModelIndex &parent) const + { + return this->rows.size(); + } + + int columnCount(const QModelIndex &parent) const + { + return this->_columnCount; + } + + QVariant data(const QModelIndex &index, int role) const + { + int row = index.row(), column = index.column(); + assert(row >= 0 && row < this->rows.size() && column >= 0 && column < this->_columnCount); + + return rows[row].items[column]->data(role); + } + + bool setData(const QModelIndex &index, const QVariant &value, int role) + { + this->rows[index.row()].items[index.column()]->setData(value, role); + + return true; + } + + QStandardItem *getItem(int row, int column) + { + assert(row >= 0 && row < this->rows.size() && column >= 0 && column < this->_columnCount); + + return rows[row][column]; + } + +protected: + virtual int prepareInsert(const TVectorItem &item, int index, + std::vector &rowToAdd) = 0; + virtual int prepareRemove(const TVectorItem &item, int index) = 0; + +private: + struct Row { + std::vector items; + bool isCustomRow; + + Row(const std::vector _items, bool _isCustomRow = false) + : items(_items) + , isCustomRow(_isCustomRow) + { + } + }; + + std::vector rows; + int _columnCount; +}; + +} // namespace util +} // namespace chatterino diff --git a/src/widgets/settingspages/commandpage.cpp b/src/widgets/settingspages/commandpage.cpp index 57bcd78db..deb91584e 100644 --- a/src/widgets/settingspages/commandpage.cpp +++ b/src/widgets/settingspages/commandpage.cpp @@ -34,68 +34,73 @@ CommandPage::CommandPage() auto layout = layoutCreator.emplace().withoutMargin(); QTableView *view = *layout.emplace(); - QStandardItemModel *model = new QStandardItemModel(0, 2, view); - view->setModel(model); - model->setHeaderData(0, Qt::Horizontal, "Trigger"); - model->setHeaderData(1, Qt::Horizontal, "Command"); - view->setSelectionMode(QAbstractItemView::ExtendedSelection); - view->setSelectionBehavior(QAbstractItemView::SelectRows); - view->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + auto *model = app->commands->createModel(this); - for (const QString &string : app->commands->getCommands()) { - int index = string.indexOf(' '); - if (index == -1) { - model->appendRow({util::stringItem(string), util::stringItem("")}); - } else { - model->appendRow( - {util::stringItem(string.mid(0, index)), util::stringItem(string.mid(index + 1))}); - } - } + // QTableView *view = *layout.emplace(); + // QStandardItemModel *model = new QStandardItemModel(0, 2, view); - QObject::connect( - model, &QStandardItemModel::dataChanged, - [model](const QModelIndex &topLeft, const QModelIndex &bottomRight, - const QVector &roles) { - QStringList list; + // view->setModel(model); + // model->setHeaderData(0, Qt::Horizontal, "Trigger"); + // model->setHeaderData(1, Qt::Horizontal, "Command"); + // view->setSelectionMode(QAbstractItemView::ExtendedSelection); + // view->setSelectionBehavior(QAbstractItemView::SelectRows); + // view->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); - for (int i = 0; i < model->rowCount(); i++) { - QString command = model->item(i, 0)->data(Qt::EditRole).toString(); - // int index = command.indexOf(' '); - // if (index != -1) { - // command = command.mid(index); - // } + // for (const QString &string : app->commands->getCommands()) { + // int index = string.indexOf(' '); + // if (index == -1) { + // model->appendRow({util::stringItem(string), util::stringItem("")}); + // } else { + // model->appendRow( + // {util::stringItem(string.mid(0, index)), util::stringItem(string.mid(index + + // 1))}); + // } + // } - list.append(command + " " + model->item(i, 1)->data(Qt::EditRole).toString()); - } + // QObject::connect( + // model, &QStandardItemModel::dataChanged, + // [model](const QModelIndex &topLeft, const QModelIndex &bottomRight, + // const QVector &roles) { + // QStringList list; - getApp()->commands->setCommands(list); - }); + // for (int i = 0; i < model->rowCount(); i++) { + // QString command = model->item(i, 0)->data(Qt::EditRole).toString(); + // // int index = command.indexOf(' '); + // // if (index != -1) { + // // command = command.mid(index); + // // } - auto buttons = layout.emplace().withoutMargin(); - { - auto add = buttons.emplace("Add"); - QObject::connect(*add, &QPushButton::clicked, [model, view] { - model->appendRow({util::stringItem("/command"), util::stringItem("")}); - view->scrollToBottom(); - }); + // list.append(command + " " + model->item(i, 1)->data(Qt::EditRole).toString()); + // } - auto remove = buttons.emplace("Remove"); - QObject::connect(*remove, &QPushButton::clicked, [view, model] { - std::vector indices; + // getApp()->commands->setCommands(list); + // }); - for (const QModelIndex &index : view->selectionModel()->selectedRows(0)) { - indices.push_back(index.row()); - } + // auto buttons = layout.emplace().withoutMargin(); + // { + // auto add = buttons.emplace("Add"); + // QObject::connect(*add, &QPushButton::clicked, [model, view] { + // model->appendRow({util::stringItem("/command"), util::stringItem("")}); + // view->scrollToBottom(); + // }); - std::sort(indices.begin(), indices.end()); + // auto remove = buttons.emplace("Remove"); + // QObject::connect(*remove, &QPushButton::clicked, [view, model] { + // std::vector indices; - for (int i = indices.size() - 1; i >= 0; i--) { - model->removeRow(indices[i]); - } - }); - buttons->addStretch(1); - } + // for (const QModelIndex &index : view->selectionModel()->selectedRows(0)) { + // indices.push_back(index.row()); + // } + + // std::sort(indices.begin(), indices.end()); + + // for (int i = indices.size() - 1; i >= 0; i--) { + // model->removeRow(indices[i]); + // } + // }); + // buttons->addStretch(1); + // } layout.append(this->createCheckBox("Also match the trigger at the end of the message", app->settings->allowCommandsAtEnd)); @@ -108,33 +113,6 @@ CommandPage::CommandPage() this->commandsEditTimer.setSingleShot(true); } -QTextEdit *CommandPage::getCommandsTextEdit() -{ - auto app = getApp(); - - // cancel - QStringList currentCommands = app->commands->getCommands(); - - this->onCancel.connect([currentCommands, app] { app->commands->setCommands(currentCommands); }); - - // create text edit - QTextEdit *textEdit = new QTextEdit; - - textEdit->setPlainText(QString(app->commands->getCommands().join('\n'))); - - QObject::connect(textEdit, &QTextEdit::textChanged, - [this] { this->commandsEditTimer.start(200); }); - - QObject::connect(&this->commandsEditTimer, &QTimer::timeout, [textEdit, app] { - QString text = textEdit->toPlainText(); - QStringList lines = text.split(QRegularExpression("(\r?\n|\r\n?)")); - - app->commands->setCommands(lines); - }); - - return textEdit; -} - } // namespace settingspages } // namespace widgets } // namespace chatterino diff --git a/src/widgets/settingspages/commandpage.hpp b/src/widgets/settingspages/commandpage.hpp index 34b67a656..40e6d9886 100644 --- a/src/widgets/settingspages/commandpage.hpp +++ b/src/widgets/settingspages/commandpage.hpp @@ -15,8 +15,6 @@ public: CommandPage(); private: - QTextEdit *getCommandsTextEdit(); - QTimer commandsEditTimer; };