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/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

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

View file

@ -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()

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
if (this->originalMessage.length() > 100) {
this->message->flags |= Message::Collapsed;
this->emplace<EmoteElement>(singletons::ResourceManager::getInstance().badgeCollapsed,
MessageElement::Collapsed);
this->emplace<EmoteElement>(getApp()->resources->badgeCollapsed, MessageElement::Collapsed);
}
#endif

View file

@ -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<QByteArray> 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<std::mutex> 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<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(' ');
@ -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

View file

@ -6,40 +6,60 @@
#include <memory>
#include <mutex>
#include <util/signalvector2.hpp>
#include <util/signalvectormodel.hpp>
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<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
//
class CommandManager
{
public:
CommandManager() = default;
CommandManager();
QString execCommand(const QString &text, std::shared_ptr<Channel> channel, bool dryRun);
void loadCommands();
void saveCommands();
void load();
void save();
void setCommands(const QStringList &commands);
QStringList getCommands();
CommandModel *createModel(QObject *parent);
util::UnsortedSignalVector<Command> commands;
private:
struct Command {
QString name;
QString text;
QMap<QString, Command> commandsMap;
Command() = default;
Command(QString text);
};
QMap<QString, Command> commands;
std::mutex mutex;
QStringList commandsStringList;
QString filePath;
QString execCustomCommand(const QStringList &words, const Command &command);

View file

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

View file

@ -1,6 +1,8 @@
#pragma once
#include <QStandardItemModel>
#include <QTimer>
#include <boost/noncopyable.hpp>
#include <pajlada/signals/signal.hpp>
#include <vector>
@ -10,18 +12,24 @@ namespace chatterino {
namespace util {
template <typename TVectorItem>
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<ItemInsertedArgs> itemInserted;
pajlada::Signals::Signal<int> itemRemoved;
pajlada::Signals::Signal<ItemArgs> itemInserted;
pajlada::Signals::Signal<ItemArgs> itemRemoved;
pajlada::Signals::NoArgSignal delayedItemsChanged;
const std::vector<TVectorItem> &getVector() const
@ -31,42 +39,56 @@ public:
return this->vector;
}
void invokeDelayedItemsChanged()
{
util::assertInGuiThread();
if (!this->itemsChangedTimer.isActive()) {
itemsChangedTimer.start();
}
}
protected:
std::vector<TVectorItem> vector;
QTimer itemsChangedTimer;
};
template <typename TVectorItem>
class BaseSignalVector : public ReadOnlySignalVector<TVectorItem>
{
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 <typename TVectorItem>
class SignalVector2 : public BaseSignalVector<TVectorItem>
class UnsortedSignalVector : public BaseSignalVector<TVectorItem>
{
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 <typename TVectorItem>
class SortedSignalVector : public BaseSignalVector<TVectorItem>
{
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);
}
};

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();
QTableView *view = *layout.emplace<QTableView>();
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<QTableView>();
// QStandardItemModel *model = new QStandardItemModel(0, 2, view);
QObject::connect(
model, &QStandardItemModel::dataChanged,
[model](const QModelIndex &topLeft, const QModelIndex &bottomRight,
const QVector<int> &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<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();
{
auto add = buttons.emplace<QPushButton>("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<QPushButton>("Remove");
QObject::connect(*remove, &QPushButton::clicked, [view, model] {
std::vector<int> indices;
// getApp()->commands->setCommands(list);
// });
for (const QModelIndex &index : view->selectionModel()->selectedRows(0)) {
indices.push_back(index.row());
}
// auto buttons = layout.emplace<QHBoxLayout>().withoutMargin();
// {
// 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--) {
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

View file

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