diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed1ebea8..9dc821bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Minor: Prevent user from entering incorrect characters in Live Notifications channels list. (#3715, #3730) - Minor: Fixed automod caught message notice appearing twice for mods. (#3717) - Minor: Added `/requests` command. Usage: `/requests [channel]`. Opens the channel points requests queue for the provided channel or the current channel if no input is provided. (#3746) +- Minor: Added ability to execute commands on chat messages using the message context menu. (#3738) - Bugfix: Fixed live notifications for usernames containing uppercase characters. (#3646) - Bugfix: Fixed live notifications not getting updated for closed streams going offline. (#3678) - Bugfix: Fixed certain settings dialogs appearing behind the main window, when `Always on top` was used. (#3679) diff --git a/src/controllers/commands/Command.cpp b/src/controllers/commands/Command.cpp index 81abe9d0a..e7113db9c 100644 --- a/src/controllers/commands/Command.cpp +++ b/src/controllers/commands/Command.cpp @@ -15,11 +15,14 @@ Command::Command(const QString &_text) this->name = _text.mid(0, index).trimmed(); this->func = _text.mid(index + 1).trimmed(); + this->showInMsgContextMenu = false; } -Command::Command(const QString &_name, const QString &_func) +Command::Command(const QString &_name, const QString &_func, + bool _showInMsgContextMenu) : name(_name.trimmed()) , func(_func.trimmed()) + , showInMsgContextMenu(_showInMsgContextMenu) { } diff --git a/src/controllers/commands/Command.hpp b/src/controllers/commands/Command.hpp index 9dd80a4bb..1b365a23f 100644 --- a/src/controllers/commands/Command.hpp +++ b/src/controllers/commands/Command.hpp @@ -10,10 +10,12 @@ namespace chatterino { struct Command { QString name; QString func; + bool showInMsgContextMenu; Command() = default; explicit Command(const QString &text); - Command(const QString &name, const QString &func); + Command(const QString &name, const QString &func, + bool showInMsgContextMenu = false); QString toString() const; }; @@ -31,6 +33,8 @@ struct Serialize { chatterino::rj::set(ret, "name", value.name, a); chatterino::rj::set(ret, "func", value.func, a); + chatterino::rj::set(ret, "showInMsgContextMenu", + value.showInMsgContextMenu, a); return ret; } @@ -59,6 +63,15 @@ struct Deserialize { PAJLADA_REPORT_ERROR(error); return command; } + if (!chatterino::rj::getSafe(value, "showInMsgContextMenu", + command.showInMsgContextMenu)) + { + command.showInMsgContextMenu = false; + + PAJLADA_REPORT_ERROR(error); + + return command; + } return command; } diff --git a/src/controllers/commands/CommandModel.cpp b/src/controllers/commands/CommandModel.cpp index d12a81c69..a3117d3ab 100644 --- a/src/controllers/commands/CommandModel.cpp +++ b/src/controllers/commands/CommandModel.cpp @@ -6,7 +6,7 @@ namespace chatterino { // commandmodel CommandModel::CommandModel(QObject *parent) - : SignalVectorModel(2, parent) + : SignalVectorModel(Column::COUNT, parent) { } @@ -14,16 +14,21 @@ CommandModel::CommandModel(QObject *parent) Command CommandModel::getItemFromRow(std::vector &row, const Command &original) { - return Command(row[0]->data(Qt::EditRole).toString(), - row[1]->data(Qt::EditRole).toString()); + return Command(row[Column::Trigger]->data(Qt::EditRole).toString(), + row[Column::CommandFunc]->data(Qt::EditRole).toString(), + row[Column::ShowInMessageContextMenu] + ->data(Qt::CheckStateRole) + .toBool()); } // turns a row in the model into a vector item void CommandModel::getRowFromItem(const Command &item, std::vector &row) { - setStringItem(row[0], item.name); - setStringItem(row[1], item.func); + setStringItem(row[Column::Trigger], item.name); + setStringItem(row[Column::CommandFunc], item.func); + setBoolItem(row[Column::ShowInMessageContextMenu], + item.showInMsgContextMenu); } } // namespace chatterino diff --git a/src/controllers/commands/CommandModel.hpp b/src/controllers/commands/CommandModel.hpp index f5c4e735a..701a65f3e 100644 --- a/src/controllers/commands/CommandModel.hpp +++ b/src/controllers/commands/CommandModel.hpp @@ -13,6 +13,13 @@ class CommandModel : public SignalVectorModel { explicit CommandModel(QObject *parent); + enum Column { + Trigger = 0, + CommandFunc = 1, + ShowInMessageContextMenu = 2, + COUNT, + }; + protected: // turn a vector item into a model row virtual Command getItemFromRow(std::vector &row, diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c938ceee2..c2fb1d2cc 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1885,6 +1885,10 @@ void ChannelView::addContextMenuItems( // Add hidden options (e.g. copy message ID) if the user held down Shift this->addHiddenContextMenuItems(hoveredElement, layout, event, *menu); + // Add executable command options + this->addCommandExecutionContextMenuItems(hoveredElement, layout, event, + *menu); + menu->popup(QCursor::pos()); menu->raise(); } @@ -2081,6 +2085,62 @@ void ChannelView::addHiddenContextMenuItems( }); } } + +void ChannelView::addCommandExecutionContextMenuItems( + const MessageLayoutElement * /*hoveredElement*/, MessageLayoutPtr layout, + QMouseEvent * /*event*/, QMenu &menu) +{ + /* Get commands to be displayed in context menu; + * only those that had the showInMsgContextMenu check box marked in the Commands page */ + std::vector cmds; + for (auto &cmd : getApp()->commands->items) + { + if (cmd.showInMsgContextMenu) + { + cmds.push_back(cmd); + } + } + + if (cmds.empty()) + { + return; + } + + menu.addSeparator(); + auto executeAction = menu.addAction("Execute command"); + auto cmdMenu = new QMenu; + executeAction->setMenu(cmdMenu); + + for (auto &cmd : cmds) + { + QString inputText = this->selection_.isEmpty() + ? layout->getMessage()->messageText + : this->getSelectedText(); + + inputText.push_front(cmd.name + " "); + + cmdMenu->addAction(cmd.name, [this, inputText] { + ChannelPtr channel; + + /* Search popups and user message history's underlyingChannels aren't of type TwitchChannel, but + * we would still like to execute commands from them. Use their source channel instead if applicable. */ + if (this->hasSourceChannel()) + { + channel = this->sourceChannel(); + } + else + { + channel = this->underlyingChannel_; + } + + QString value = + getApp()->commands->execCommand(inputText, channel, false); + + channel->sendMessage(value); + }); + } +} + void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 05f2becb0..994f1e762 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -184,6 +184,10 @@ private: void addHiddenContextMenuItems(const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, QMouseEvent *event, QMenu &menu); + void addCommandExecutionContextMenuItems( + const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, + QMouseEvent *event, QMenu &menu); + int getLayoutWidth() const; void updatePauses(); void unpaused(); diff --git a/src/widgets/settingspages/CommandPage.cpp b/src/widgets/settingspages/CommandPage.cpp index ea43105e1..a202f46ae 100644 --- a/src/widgets/settingspages/CommandPage.cpp +++ b/src/widgets/settingspages/CommandPage.cpp @@ -44,8 +44,9 @@ CommandPage::CommandPage() layout.emplace(app->commands->createModel(nullptr)) .getElement(); - view->setTitles({"Trigger", "Command"}); - view->getTableView()->horizontalHeader()->setStretchLastSection(true); + view->setTitles({"Trigger", "Command", "Show In\nMessage Menu"}); + view->getTableView()->horizontalHeader()->setSectionResizeMode( + 1, QHeaderView::Stretch); view->addButtonPressed.connect([] { getApp()->commands->items.append( Command{"/command", "I made a new command HeyGuys"});