added template model/view magic for commands

This commit is contained in:
fourtf 2018-04-29 23:25:49 +02:00
parent e31dc09e91
commit 6bd787423d
14 changed files with 326 additions and 165 deletions

View file

@ -184,7 +184,9 @@ SOURCES += \
src/widgets/lastruncrashdialog.cpp \ src/widgets/lastruncrashdialog.cpp \
src/widgets/attachedwindow.cpp \ src/widgets/attachedwindow.cpp \
src/widgets/settingspages/externaltoolspage.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 += \ HEADERS += \
src/precompiled_header.hpp \ src/precompiled_header.hpp \
@ -316,7 +318,9 @@ HEADERS += \
src/util/standarditemhelper.hpp \ src/util/standarditemhelper.hpp \
src/widgets/helper/comboboxitemdelegate.hpp \ src/widgets/helper/comboboxitemdelegate.hpp \
src/util/assertinguithread.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/resources.qrc resources/resources.qrc

@ -1 +1 @@
Subproject commit 94edfacf14728faf3aa1d9c058e89395c97aae14 Subproject commit ad31b38866d80a17ced902476ed06da69edce3a0

View file

@ -93,7 +93,7 @@ void Application::initialize()
this->nativeMessaging->registerHost(); this->nativeMessaging->registerHost();
this->settings->load(); this->settings->load();
this->commands->loadCommands(); this->commands->load();
this->emotes->loadGlobalEmotes(); this->emotes->loadGlobalEmotes();
@ -218,7 +218,7 @@ void Application::save()
{ {
this->windows->save(); this->windows->save();
this->commands->saveCommands(); this->commands->save();
} }
void Application::runNativeMessagingHost() void Application::runNativeMessagingHost()

View file

View file

@ -0,0 +1,9 @@
#pragma once
namespace chatterino {
namespace managers {
namespace commands {
// code
}
} // namespace managers
} // namespace chatterino

View file

@ -74,8 +74,7 @@ MessagePtr TwitchMessageBuilder::build()
#ifdef XD #ifdef XD
if (this->originalMessage.length() > 100) { if (this->originalMessage.length() > 100) {
this->message->flags |= Message::Collapsed; this->message->flags |= Message::Collapsed;
this->emplace<EmoteElement>(singletons::ResourceManager::getInstance().badgeCollapsed, this->emplace<EmoteElement>(getApp()->resources->badgeCollapsed, MessageElement::Collapsed);
MessageElement::Collapsed);
} }
#endif #endif

View file

@ -19,7 +19,24 @@ using namespace chatterino::providers::twitch;
namespace chatterino { namespace chatterino {
namespace singletons { 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(); auto app = getApp();
this->filePath = app->paths->customFolderPath + "/Commands.txt"; this->filePath = app->paths->customFolderPath + "/Commands.txt";
@ -32,18 +49,14 @@ void CommandManager::loadCommands()
QList<QByteArray> test = textFile.readAll().split('\n'); QList<QByteArray> test = textFile.readAll().split('\n');
QStringList loadedCommands;
for (const auto &command : test) { for (const auto &command : test) {
loadedCommands.append(command); this->commands.appendItem(Command(command));
} }
this->setCommands(loadedCommands);
textFile.close(); textFile.close();
} }
void CommandManager::saveCommands() void CommandManager::save()
{ {
QFile textFile(this->filePath); QFile textFile(this->filePath);
if (!textFile.open(QIODevice::WriteOnly)) { if (!textFile.open(QIODevice::WriteOnly)) {
@ -51,44 +64,16 @@ void CommandManager::saveCommands()
return; return;
} }
QString commandsString = this->commandsStringList.join('\n'); for (const Command &cmd : this->commands.getVector()) {
textFile.write((cmd.toString() + "\n").toUtf8());
textFile.write(commandsString.toUtf8()); }
textFile.close(); textFile.close();
} }
void CommandManager::setCommands(const QStringList &_commands) CommandModel *CommandManager::createModel(QObject *parent)
{ {
std::lock_guard<std::mutex> lock(this->mutex); return new CommandModel(&this->commands, parent);
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;
} }
QString CommandManager::execCommand(const QString &text, ChannelPtr channel, bool dryRun) 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 // check if custom command exists
auto it = this->commands.find(commandName); auto it = this->commandsMap.find(commandName);
if (it == this->commandsMap.end()) {
if (it == this->commands.end()) {
return text; return text;
} }
@ -193,17 +177,17 @@ QString CommandManager::execCustomCommand(const QStringList &words, const Comman
int lastCaptureEnd = 0; int lastCaptureEnd = 0;
auto globalMatch = parseCommand.globalMatch(command.text); auto globalMatch = parseCommand.globalMatch(command.func);
int matchOffset = 0; int matchOffset = 0;
while (true) { while (true) {
QRegularExpressionMatch match = parseCommand.match(command.text, matchOffset); QRegularExpressionMatch match = parseCommand.match(command.func, matchOffset);
if (!match.hasMatch()) { if (!match.hasMatch()) {
break; break;
} }
result += command.text.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1); result += command.func.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1);
lastCaptureEnd = match.capturedEnd(); lastCaptureEnd = match.capturedEnd();
matchOffset = lastCaptureEnd - 1; 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) == '{') { if (result.size() > 0 && result.at(0) == '{') {
result = result.mid(1); result = result.mid(1);
@ -247,7 +231,30 @@ QString CommandManager::execCustomCommand(const QStringList &words, const Comman
return result.replace("{{", "{"); return result.replace("{{", "{");
} }
CommandManager::Command::Command(QString _text) // commandmodel
CommandModel::CommandModel(util::BaseSignalVector<Command> *vec, QObject *parent)
: util::SignalVectorModel<Command>(vec, 2, parent)
{
}
int CommandModel::prepareInsert(const Command &item, int index,
std::vector<QStandardItem *> &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(' '); int index = _text.indexOf(' ');
@ -257,7 +264,18 @@ CommandManager::Command::Command(QString _text)
} }
this->name = _text.mid(0, index); 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 } // namespace singletons

View file

@ -6,40 +6,60 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <util/signalvector2.hpp>
#include <util/signalvectormodel.hpp>
namespace chatterino { namespace chatterino {
class Channel; class Channel;
namespace singletons { 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<Command>
{
explicit CommandModel(util::BaseSignalVector<Command> *vec, QObject *parent);
protected:
virtual int prepareInsert(const Command &item, int index,
std::vector<QStandardItem *> &rowToAdd) override;
virtual int prepareRemove(const Command &item, int index) override;
friend class CommandManager;
};
// //
// this class managed the custom /commands // this class managed the custom /commands
// //
class CommandManager class CommandManager
{ {
public: public:
CommandManager() = default; CommandManager();
QString execCommand(const QString &text, std::shared_ptr<Channel> channel, bool dryRun); QString execCommand(const QString &text, std::shared_ptr<Channel> channel, bool dryRun);
void loadCommands(); void load();
void saveCommands(); void save();
void setCommands(const QStringList &commands); CommandModel *createModel(QObject *parent);
QStringList getCommands();
util::UnsortedSignalVector<Command> commands;
private: private:
struct Command { QMap<QString, Command> commandsMap;
QString name;
QString text;
Command() = default;
Command(QString text);
};
QMap<QString, Command> commands;
std::mutex mutex; std::mutex mutex;
QStringList commandsStringList;
QString filePath; QString filePath;
QString execCustomCommand(const QStringList &words, const Command &command); QString execCustomCommand(const QStringList &words, const Command &command);

View file

@ -7,7 +7,7 @@
namespace chatterino { namespace chatterino {
namespace util { namespace util {
void assertInGuiThread() static void assertInGuiThread()
{ {
#ifdef _DEBUG #ifdef _DEBUG
assert(QCoreApplication::instance()->thread() == QThread::currentThread()); assert(QCoreApplication::instance()->thread() == QThread::currentThread());

View file

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QTimer>
#include <boost/noncopyable.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <vector> #include <vector>
@ -10,18 +12,24 @@ namespace chatterino {
namespace util { namespace util {
template <typename TVectorItem> template <typename TVectorItem>
class ReadOnlySignalVector class ReadOnlySignalVector : boost::noncopyable
{ {
public: public:
ReadOnlySignalVector()
{
QObject::connect(&this->itemsChangedTimer, &QTimer::timeout,
[this] { this->delayedItemsChanged.invoke(); });
}
virtual ~ReadOnlySignalVector() = default; virtual ~ReadOnlySignalVector() = default;
struct ItemInsertedArgs { struct ItemArgs {
const TVectorItem &item; const TVectorItem &item;
int index; int index;
void *caller;
}; };
pajlada::Signals::Signal<ItemInsertedArgs> itemInserted; pajlada::Signals::Signal<ItemArgs> itemInserted;
pajlada::Signals::Signal<int> itemRemoved; pajlada::Signals::Signal<ItemArgs> itemRemoved;
pajlada::Signals::NoArgSignal delayedItemsChanged; pajlada::Signals::NoArgSignal delayedItemsChanged;
const std::vector<TVectorItem> &getVector() const const std::vector<TVectorItem> &getVector() const
@ -31,42 +39,56 @@ public:
return this->vector; return this->vector;
} }
void invokeDelayedItemsChanged()
{
util::assertInGuiThread();
if (!this->itemsChangedTimer.isActive()) {
itemsChangedTimer.start();
}
}
protected: protected:
std::vector<TVectorItem> vector; std::vector<TVectorItem> vector;
QTimer itemsChangedTimer;
}; };
template <typename TVectorItem> template <typename TVectorItem>
class BaseSignalVector : public ReadOnlySignalVector<TVectorItem> class BaseSignalVector : public ReadOnlySignalVector<TVectorItem>
{ {
public: public:
void removeItem(int index) virtual void appendItem(const TVectorItem &item, void *caller = 0) = 0;
void removeItem(int index, void *caller = 0)
{ {
util::assertInGuiThread(); util::assertInGuiThread();
assert(index >= 0 && index < this->vector.size()); assert(index >= 0 && index < this->vector.size());
TVectorItem item = this->vector[index];
this->vector.erase(this->vector.begin() + index); this->vector.erase(this->vector.begin() + index);
this->itemRemoved.invoke(index); ItemArgs args{item, args, caller};
this->itemRemoved.invoke(args);
} }
}; };
template <typename TVectorItem> template <typename TVectorItem>
class SignalVector2 : public BaseSignalVector<TVectorItem> class UnsortedSignalVector : public BaseSignalVector<TVectorItem>
{ {
public: public:
void insertItem(const TVectorItem &item, int index) void insertItem(const TVectorItem &item, int index, void *caller = 0)
{ {
util::assertInGuiThread(); util::assertInGuiThread();
assert(index >= 0 && index <= this->vector.size()); assert(index >= 0 && index <= this->vector.size());
this->vector.insert(this->vector.begin() + index, item); this->vector.insert(this->vector.begin() + index, item);
ItemInsertedArgs args{item, index}; ItemArgs args{item, index, caller};
this->itemInserted.invoke(args); 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 <typename TVectorItem>
class SortedSignalVector : public BaseSignalVector<TVectorItem> class SortedSignalVector : public BaseSignalVector<TVectorItem>
{ {
public: public:
void addItem(const TVectorItem &item) virtual void appendItem(const TVectorItem &item, void *caller = 0) override
{ {
util::assertInGuiThread(); util::assertInGuiThread();
int index = this->vector.insert( int index = this->vector.insert(
std::lower_bound(this->vector.begin(), this->vector.end(), item), item) - std::lower_bound(this->vector.begin(), this->vector.end(), item), item) -
this->vector.begin(); this->vector.begin();
ItemInsertedArgs args{item, index}; ItemArgs args{item, index, caller};
this->itemInserted.invoke(args); this->itemInserted.invoke(args);
} }
}; };

View file

@ -0,0 +1 @@
#include "signalvectormodel.hpp"

View file

@ -0,0 +1,112 @@
#pragma once
#include <QAbstractTableModel>
#include <QStandardItem>
#include <util/signalvector2.hpp>
#include <pajlada/signals/signalholder.hpp>
namespace chatterino {
namespace util {
template <typename TVectorItem>
class SignalVectorModel : public QAbstractTableModel, pajlada::Signals::SignalHolder
{
public:
SignalVectorModel(util::BaseSignalVector<TVectorItem> *vec, int columnCount,
QObject *parent = nullptr)
: QAbstractTableModel(parent)
, _columnCount(columnCount)
{
this->managedConnect(vec->itemInserted, [this](auto args) {
std::vector<QStandardItem *> 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<QStandardItem *> &rowToAdd) = 0;
virtual int prepareRemove(const TVectorItem &item, int index) = 0;
private:
struct Row {
std::vector<QStandardItem *> items;
bool isCustomRow;
Row(const std::vector<QStandardItem *> _items, bool _isCustomRow = false)
: items(_items)
, isCustomRow(_isCustomRow)
{
}
};
std::vector<Row> rows;
int _columnCount;
};
} // namespace util
} // namespace chatterino

View file

@ -34,68 +34,73 @@ CommandPage::CommandPage()
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin(); auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
QTableView *view = *layout.emplace<QTableView>(); QTableView *view = *layout.emplace<QTableView>();
QStandardItemModel *model = new QStandardItemModel(0, 2, view);
view->setModel(model); auto *model = app->commands->createModel(this);
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 (const QString &string : app->commands->getCommands()) { // QTableView *view = *layout.emplace<QTableView>();
int index = string.indexOf(' '); // QStandardItemModel *model = new QStandardItemModel(0, 2, view);
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))});
}
}
QObject::connect( // view->setModel(model);
model, &QStandardItemModel::dataChanged, // model->setHeaderData(0, Qt::Horizontal, "Trigger");
[model](const QModelIndex &topLeft, const QModelIndex &bottomRight, // model->setHeaderData(1, Qt::Horizontal, "Command");
const QVector<int> &roles) { // view->setSelectionMode(QAbstractItemView::ExtendedSelection);
QStringList list; // view->setSelectionBehavior(QAbstractItemView::SelectRows);
// view->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
for (int i = 0; i < model->rowCount(); i++) { // for (const QString &string : app->commands->getCommands()) {
QString command = model->item(i, 0)->data(Qt::EditRole).toString(); // int index = string.indexOf(' ');
// int index = command.indexOf(' '); // if (index == -1) {
// if (index != -1) { // model->appendRow({util::stringItem(string), util::stringItem("")});
// command = command.mid(index); // } 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<int> &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<QHBoxLayout>().withoutMargin(); // list.append(command + " " + model->item(i, 1)->data(Qt::EditRole).toString());
{ // }
auto add = buttons.emplace<QPushButton>("Add");
QObject::connect(*add, &QPushButton::clicked, [model, view] {
model->appendRow({util::stringItem("/command"), util::stringItem("")});
view->scrollToBottom();
});
auto remove = buttons.emplace<QPushButton>("Remove"); // getApp()->commands->setCommands(list);
QObject::connect(*remove, &QPushButton::clicked, [view, model] { // });
std::vector<int> indices;
for (const QModelIndex &index : view->selectionModel()->selectedRows(0)) { // auto buttons = layout.emplace<QHBoxLayout>().withoutMargin();
indices.push_back(index.row()); // {
} // auto add = buttons.emplace<QPushButton>("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<QPushButton>("Remove");
// QObject::connect(*remove, &QPushButton::clicked, [view, model] {
// std::vector<int> indices;
for (int i = indices.size() - 1; i >= 0; i--) { // for (const QModelIndex &index : view->selectionModel()->selectedRows(0)) {
model->removeRow(indices[i]); // indices.push_back(index.row());
} // }
});
buttons->addStretch(1); // 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", layout.append(this->createCheckBox("Also match the trigger at the end of the message",
app->settings->allowCommandsAtEnd)); app->settings->allowCommandsAtEnd));
@ -108,33 +113,6 @@ CommandPage::CommandPage()
this->commandsEditTimer.setSingleShot(true); 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 settingspages
} // namespace widgets } // namespace widgets
} // namespace chatterino } // namespace chatterino

View file

@ -15,8 +15,6 @@ public:
CommandPage(); CommandPage();
private: private:
QTextEdit *getCommandsTextEdit();
QTimer commandsEditTimer; QTimer commandsEditTimer;
}; };